Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Зуев Егор
/
wiki.dev
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Authored by
Dan Brown
2016-09-05 19:35:21 +0100
Browse Files
Options
Browse Files
Tag
Download
Plain Diff
Commit
7caed3b0dbea4d289747d19b30893f4bcdef94e6
7caed3b0
2 parents
45641d07
11960d9d
Merge branch 'master' into release
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
44 changed files
with
657 additions
and
238 deletions
.github/ISSUE_TEMPLATE.md
.travis.yml
app/Entity.php
app/Exceptions/Handler.php
app/Exceptions/PrettyException.php
app/Http/Controllers/BookController.php
app/Http/Controllers/ChapterController.php
app/PageRevision.php
app/Providers/AppServiceProvider.php
app/Repos/BookRepo.php
app/Repos/ChapterRepo.php
app/Repos/EntityRepo.php
app/Repos/PageRepo.php
app/Services/ExportService.php
app/Services/ImageService.php
app/Services/PermissionService.php
app/Services/SocialAuthService.php
app/helpers.php
database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php
phpunit.xml
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/js/global.js
resources/assets/js/pages/page-form.js
resources/assets/sass/_buttons.scss
resources/assets/sass/_image-manager.scss → resources/assets/sass/_components.scss
resources/assets/sass/_grid.scss
resources/assets/sass/_header.scss
resources/assets/sass/_lists.scss
resources/assets/sass/_pages.scss
resources/assets/sass/styles.scss
resources/views/books/list-item.blade.php
resources/views/books/sort.blade.php
resources/views/chapters/list-item.blade.php
resources/views/pages/edit.blade.php
resources/views/pages/form.blade.php
resources/views/pages/list-item.blade.php
resources/views/pages/restrictions.blade.php
resources/views/pages/revisions.blade.php
resources/views/pages/show.blade.php
resources/views/partials/entity-selector-popup.blade.php
resources/views/partials/image-manager.blade.php
tests/Entity/EntitySearchTest.php
tests/Entity/EntityTest.php
.github/ISSUE_TEMPLATE.md
0 → 100644
View file @
7caed3b
### For Feature Requests
Desired Feature:
### For Bug Reports
PHP Version:
MySQL Version:
Expected Behavior:
Actual Behavior:
.travis.yml
View file @
7caed3b
...
...
@@ -6,8 +6,6 @@ php:
cache
:
directories
:
-
vendor
-
node_modules
-
$HOME/.composer/cache
addons
:
...
...
@@ -17,19 +15,17 @@ addons:
-
mysql-client-core-5.6
-
mysql-client-5.6
before_install
:
-
npm install -g npm@latest
before_script
:
-
mysql -u root -e 'create database `bookstack-test`;'
-
composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
-
phpenv config-rm xdebug.ini
-
composer self-update
-
composer dump-autoload --no-interaction
-
composer install --prefer-dist --no-interaction
-
npm install
-
./node_modules/.bin/gulp
-
php artisan clear-compiled -n
-
php artisan optimize -n
-
php artisan migrate --force -n --database=mysql_testing
-
php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
script
:
-
vendor/bin/phpunit
\ No newline at end of file
-
phpunit
\ No newline at end of file
...
...
app/Entity.php
View file @
7caed3b
...
...
@@ -167,7 +167,8 @@ class Entity extends Ownable
foreach
(
$terms
as
$key
=>
$term
)
{
$term
=
htmlentities
(
$term
,
ENT_QUOTES
);
$term
=
preg_replace
(
'/[+\-><\(\)~*\"@]+/'
,
' '
,
$term
);
if
(
preg_match
(
'/\s/'
,
$term
))
{
if
(
preg_match
(
'/".*?"/'
,
$term
))
{
$term
=
str_replace
(
'"'
,
''
,
$term
);
$exactTerms
[]
=
'%'
.
$term
.
'%'
;
$term
=
'"'
.
$term
.
'"'
;
}
else
{
...
...
app/Exceptions/Handler.php
View file @
7caed3b
...
...
@@ -47,19 +47,44 @@ class Handler extends ExceptionHandler
{
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if
(
$
e
instanceof
NotifyException
)
{
session
()
->
flash
(
'error'
,
$
e
->
message
);
if
(
$
this
->
isExceptionType
(
$e
,
NotifyException
::
class
)
)
{
session
()
->
flash
(
'error'
,
$
this
->
getOriginalMessage
(
$e
)
);
return
redirect
(
$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
(
);
if
(
$this
->
isExceptionType
(
$e
,
PrettyException
::
class
)
&&
!
config
(
'app.debug'
))
{
$message
=
$this
->
getOriginalMessage
(
$e
);
$code
=
(
$e
->
getCode
()
===
0
)
?
500
:
$e
->
getCode
();
return
response
()
->
view
(
'errors/'
.
$code
,
[
'message'
=>
$message
],
$code
);
}
return
parent
::
render
(
$request
,
$e
);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
* @param $type
* @return bool
*/
protected
function
isExceptionType
(
Exception
$e
,
$type
)
{
do
{
if
(
is_a
(
$e
,
$type
))
return
true
;
}
while
(
$e
=
$e
->
getPrevious
());
return
false
;
}
/**
* Get original exception message.
* @param Exception $e
* @return string
*/
protected
function
getOriginalMessage
(
Exception
$e
)
{
do
{
$message
=
$e
->
getMessage
();
}
while
(
$e
=
$e
->
getPrevious
());
return
$message
;
}
}
...
...
app/Exceptions/PrettyException.php
View file @
7caed3b
<?php
namespace
BookStack\Exceptions
;
use
Exception
;
class
PrettyException
extends
Exception
{}
\ No newline at end of file
class
PrettyException
extends
\Exception
{}
\ No newline at end of file
...
...
app/Http/Controllers/BookController.php
View file @
7caed3b
...
...
@@ -3,7 +3,6 @@
use
Activity
;
use
BookStack\Repos\UserRepo
;
use
Illuminate\Http\Request
;
use
Illuminate\Support\Facades\Auth
;
use
BookStack\Http\Requests
;
use
BookStack\Repos\BookRepo
;
use
BookStack\Repos\ChapterRepo
;
...
...
@@ -180,21 +179,31 @@ class BookController extends Controller
return
redirect
(
$book
->
getUrl
());
}
$sortedBooks
=
[];
// Sort pages and chapters
$sortedBooks
=
[];
$updatedModels
=
collect
();
$sortMap
=
json_decode
(
$request
->
get
(
'sort-tree'
));
$defaultBookId
=
$book
->
id
;
foreach
(
$sortMap
as
$index
=>
$bookChild
)
{
$id
=
$bookChild
->
id
;
// Loop through contents of provided map and update entities accordingly
foreach
(
$sortMap
as
$bookChild
)
{
$priority
=
$bookChild
->
sort
;
$id
=
intval
(
$bookChild
->
id
);
$isPage
=
$bookChild
->
type
==
'page'
;
$bookId
=
$this
->
bookRepo
->
exists
(
$bookChild
->
book
)
?
$bookChild
->
book
:
$defaultBookId
;
$bookId
=
$this
->
bookRepo
->
exists
(
$bookChild
->
book
)
?
intval
(
$bookChild
->
book
)
:
$defaultBookId
;
$chapterId
=
(
$isPage
&&
$bookChild
->
parentChapter
===
false
)
?
0
:
intval
(
$bookChild
->
parentChapter
);
$model
=
$isPage
?
$this
->
pageRepo
->
getById
(
$id
)
:
$this
->
chapterRepo
->
getById
(
$id
);
// Update models only if there's a change in parent chain or ordering.
if
(
$model
->
priority
!==
$priority
||
$model
->
book_id
!==
$bookId
||
(
$isPage
&&
$model
->
chapter_id
!==
$chapterId
))
{
$isPage
?
$this
->
pageRepo
->
changeBook
(
$bookId
,
$model
)
:
$this
->
chapterRepo
->
changeBook
(
$bookId
,
$model
);
$model
->
priority
=
$index
;
if
(
$isPage
)
{
$model
->
chapter_id
=
(
$bookChild
->
parentChapter
===
false
)
?
0
:
$bookChild
->
parentChapter
;
}
$model
->
priority
=
$priority
;
if
(
$isPage
)
$model
->
chapter_id
=
$chapterId
;
$model
->
save
();
$updatedModels
->
push
(
$model
);
}
// Store involved books to be sorted later
if
(
!
in_array
(
$bookId
,
$sortedBooks
))
{
$sortedBooks
[]
=
$bookId
;
}
...
...
@@ -203,10 +212,12 @@ class BookController extends Controller
// Add activity for books
foreach
(
$sortedBooks
as
$bookId
)
{
$updatedBook
=
$this
->
bookRepo
->
getById
(
$bookId
);
$this
->
bookRepo
->
updateBookPermissions
(
$updatedBook
);
Activity
::
add
(
$updatedBook
,
'book_sort'
,
$updatedBook
->
id
);
}
// Update permissions on changed models
$this
->
bookRepo
->
buildJointPermissions
(
$updatedModels
);
return
redirect
(
$book
->
getUrl
());
}
...
...
app/Http/Controllers/ChapterController.php
View file @
7caed3b
...
...
@@ -204,7 +204,7 @@ class ChapterController extends Controller
return
redirect
()
->
back
();
}
$this
->
chapterRepo
->
changeBook
(
$parent
->
id
,
$chapter
);
$this
->
chapterRepo
->
changeBook
(
$parent
->
id
,
$chapter
,
true
);
Activity
::
add
(
$chapter
,
'chapter_move'
,
$chapter
->
book
->
id
);
session
()
->
flash
(
'success'
,
sprintf
(
'Chapter moved to "%s"'
,
$parent
->
name
));
...
...
app/PageRevision.php
View file @
7caed3b
...
...
@@ -3,7 +3,7 @@
class
PageRevision
extends
Model
{
protected
$fillable
=
[
'name'
,
'html'
,
'text'
,
'markdown'
];
protected
$fillable
=
[
'name'
,
'html'
,
'text'
,
'markdown'
,
'summary'
];
/**
* Get the user that created the page revision
...
...
app/Providers/AppServiceProvider.php
View file @
7caed3b
<?php
<?php
namespace
BookStack\Providers
;
namespace
BookStack\Providers
;
use
Illuminate\Support\Facades\Auth
;
use
Illuminate\Support\ServiceProvider
;
use
BookStack\User
;
class
AppServiceProvider
extends
ServiceProvider
{
...
...
app/Repos/BookRepo.php
View file @
7caed3b
...
...
@@ -2,6 +2,7 @@
use
Alpha\B
;
use
BookStack\Exceptions\NotFoundException
;
use
Illuminate\Database\Eloquent\Collection
;
use
Illuminate\Support\Str
;
use
BookStack\Book
;
use
Views
;
...
...
@@ -174,15 +175,6 @@ class BookRepo extends EntityRepo
}
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Book $book
*/
public
function
updateBookPermissions
(
Book
$book
)
{
$this
->
permissionService
->
buildJointPermissionsForEntity
(
$book
);
}
/**
* Get the next child element priority.
* @param Book $book
* @return int
...
...
app/Repos/ChapterRepo.php
View file @
7caed3b
...
...
@@ -197,9 +197,10 @@ class ChapterRepo extends EntityRepo
* Changes the book relation of this chapter.
* @param $bookId
* @param Chapter $chapter
* @param bool $rebuildPermissions
* @return Chapter
*/
public
function
changeBook
(
$bookId
,
Chapter
$chapter
)
public
function
changeBook
(
$bookId
,
Chapter
$chapter
,
$rebuildPermissions
=
false
)
{
$chapter
->
book_id
=
$bookId
;
// Update related activity
...
...
@@ -213,9 +214,12 @@ class ChapterRepo extends EntityRepo
foreach
(
$chapter
->
pages
as
$page
)
{
$this
->
pageRepo
->
changeBook
(
$bookId
,
$page
);
}
// Update permissions
// Update permissions if applicable
if
(
$rebuildPermissions
)
{
$chapter
->
load
(
'book'
);
$this
->
permissionService
->
buildJointPermissionsForEntity
(
$chapter
->
book
);
}
return
$chapter
;
}
...
...
app/Repos/EntityRepo.php
View file @
7caed3b
...
...
@@ -6,6 +6,7 @@ use BookStack\Entity;
use
BookStack\Page
;
use
BookStack\Services\PermissionService
;
use
BookStack\User
;
use
Illuminate\Support\Collection
;
use
Illuminate\Support\Facades\Log
;
class
EntityRepo
...
...
@@ -168,15 +169,16 @@ class EntityRepo
* @param $termString
* @return array
*/
p
rotected
function
prepareSearchTerms
(
$termString
)
p
ublic
function
prepareSearchTerms
(
$termString
)
{
$termString
=
$this
->
cleanSearchTermString
(
$termString
);
preg_match_all
(
'/"(.*?)"/'
,
$termString
,
$matches
);
preg_match_all
(
'/(".*?")/'
,
$termString
,
$matches
);
$terms
=
[];
if
(
count
(
$matches
[
1
])
>
0
)
{
$terms
=
$matches
[
1
];
foreach
(
$matches
[
1
]
as
$match
)
{
$terms
[]
=
$match
;
}
$termString
=
trim
(
preg_replace
(
'/"(.*?)"/'
,
''
,
$termString
));
}
else
{
$terms
=
[];
}
if
(
!
empty
(
$termString
))
$terms
=
array_merge
(
$terms
,
explode
(
' '
,
$termString
));
return
$terms
;
...
...
@@ -259,6 +261,15 @@ class EntityRepo
return
$query
;
}
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities
*/
public
function
buildJointPermissions
(
Collection
$collection
)
{
$this
->
permissionService
->
buildJointPermissionsForEntities
(
$collection
);
}
}
...
...
app/Repos/PageRepo.php
View file @
7caed3b
...
...
@@ -157,6 +157,8 @@ class PageRepo extends EntityRepo
$draftPage
->
draft
=
false
;
$draftPage
->
save
();
$this
->
saveRevision
(
$draftPage
,
'Initial Publish'
);
return
$draftPage
;
}
...
...
@@ -308,10 +310,9 @@ class PageRepo extends EntityRepo
*/
public
function
updatePage
(
Page
$page
,
$book_id
,
$input
)
{
// Save a revision before updating
if
(
$page
->
html
!==
$input
[
'html'
]
||
$page
->
name
!==
$input
[
'name'
])
{
$this
->
saveRevision
(
$page
);
}
// Hold the old details to compare later
$oldHtml
=
$page
->
html
;
$oldName
=
$page
->
name
;
// Prevent slug being updated if no name change
if
(
$page
->
name
!==
$input
[
'name'
])
{
...
...
@@ -335,6 +336,11 @@ class PageRepo extends EntityRepo
// Remove all update drafts for this user & page.
$this
->
userUpdateDraftsQuery
(
$page
,
$userId
)
->
delete
();
// Save a revision after updating
if
(
$oldHtml
!==
$input
[
'html'
]
||
$oldName
!==
$input
[
'name'
]
||
$input
[
'summary'
]
!==
null
)
{
$this
->
saveRevision
(
$page
,
$input
[
'summary'
]);
}
return
$page
;
}
...
...
@@ -360,9 +366,10 @@ class PageRepo extends EntityRepo
/**
* Saves a page revision into the system.
* @param Page $page
* @param null|string $summary
* @return $this
*/
public
function
saveRevision
(
Page
$page
)
public
function
saveRevision
(
Page
$page
,
$summary
=
null
)
{
$revision
=
$this
->
pageRevision
->
fill
(
$page
->
toArray
());
if
(
setting
(
'app-editor'
)
!==
'markdown'
)
$revision
->
markdown
=
''
;
...
...
@@ -372,6 +379,7 @@ class PageRepo extends EntityRepo
$revision
->
created_by
=
auth
()
->
user
()
->
id
;
$revision
->
created_at
=
$page
->
updated_at
;
$revision
->
type
=
'version'
;
$revision
->
summary
=
$summary
;
$revision
->
save
();
// Clear old revisions
if
(
$this
->
pageRevision
->
where
(
'page_id'
,
'='
,
$page
->
id
)
->
count
()
>
50
)
{
...
...
app/Services/ExportService.php
View file @
7caed3b
...
...
@@ -48,11 +48,13 @@ class ExportService
foreach
(
$imageTagsOutput
[
0
]
as
$index
=>
$imgMatch
)
{
$oldImgString
=
$imgMatch
;
$srcString
=
$imageTagsOutput
[
2
][
$index
];
if
(
strpos
(
trim
(
$srcString
),
'http'
)
!==
0
)
{
$pathString
=
public_path
(
$srcString
);
$isLocal
=
strpos
(
trim
(
$srcString
),
'http'
)
!==
0
;
if
(
$isLocal
)
{
$pathString
=
public_path
(
trim
(
$srcString
,
'/'
));
}
else
{
$pathString
=
$srcString
;
}
if
(
$isLocal
&&
!
file_exists
(
$pathString
))
continue
;
$imageContent
=
file_get_contents
(
$pathString
);
$imageEncoded
=
'data:image/'
.
pathinfo
(
$pathString
,
PATHINFO_EXTENSION
)
.
';base64,'
.
base64_encode
(
$imageContent
);
$newImageString
=
str_replace
(
$srcString
,
$imageEncoded
,
$oldImgString
);
...
...
app/Services/ImageService.php
View file @
7caed3b
...
...
@@ -95,6 +95,7 @@ class ImageService
try
{
$storage
->
put
(
$fullPath
,
$imageData
);
$storage
->
setVisibility
(
$fullPath
,
'public'
);
}
catch
(
Exception
$e
)
{
throw
new
ImageUploadException
(
'Image Path '
.
$fullPath
.
' is not writable by the server.'
);
}
...
...
@@ -167,6 +168,7 @@ class ImageService
$thumbData
=
(
string
)
$thumb
->
encode
();
$storage
->
put
(
$thumbFilePath
,
$thumbData
);
$storage
->
setVisibility
(
$thumbFilePath
,
'public'
);
$this
->
cache
->
put
(
'images-'
.
$image
->
id
.
'-'
.
$thumbFilePath
,
$thumbFilePath
,
60
*
72
);
return
$this
->
getPublicUrl
(
$thumbFilePath
);
...
...
@@ -257,10 +259,16 @@ class ImageService
$storageUrl
=
config
(
'filesystems.url'
);
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if
(
$storageUrl
==
false
&&
config
(
'filesystems.default'
)
===
's3'
)
{
$storageDetails
=
config
(
'filesystems.disks.s3'
);
if
(
strpos
(
$storageDetails
[
'bucket'
],
'.'
)
===
false
)
{
$storageUrl
=
'https://'
.
$storageDetails
[
'bucket'
]
.
'.s3.amazonaws.com'
;
}
else
{
$storageUrl
=
'https://s3-'
.
$storageDetails
[
'region'
]
.
'.amazonaws.com/'
.
$storageDetails
[
'bucket'
];
}
}
$this
->
storageUrl
=
$storageUrl
;
}
...
...
app/Services/PermissionService.php
View file @
7caed3b
...
...
@@ -8,7 +8,7 @@ use BookStack\Ownable;
use
BookStack\Page
;
use
BookStack\Role
;
use
BookStack\User
;
use
Illuminate\
Database\Eloquen
t\Collection
;
use
Illuminate\
Suppor
t\Collection
;
class
PermissionService
{
...
...
@@ -25,6 +25,8 @@ class PermissionService
protected
$jointPermission
;
protected
$role
;
protected
$entityCache
;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
...
...
@@ -49,6 +51,57 @@ class PermissionService
}
/**
* Prepare the local entity cache and ensure it's empty
*/
protected
function
readyEntityCache
()
{
$this
->
entityCache
=
[
'books'
=>
collect
(),
'chapters'
=>
collect
()
];
}
/**
* Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/
protected
function
getBook
(
$bookId
)
{
if
(
isset
(
$this
->
entityCache
[
'books'
])
&&
$this
->
entityCache
[
'books'
]
->
has
(
$bookId
))
{
return
$this
->
entityCache
[
'books'
]
->
get
(
$bookId
);
}
$book
=
$this
->
book
->
find
(
$bookId
);
if
(
$book
===
null
)
$book
=
false
;
if
(
isset
(
$this
->
entityCache
[
'books'
]))
{
$this
->
entityCache
[
'books'
]
->
put
(
$bookId
,
$book
);
}
return
$book
;
}
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return Book
*/
protected
function
getChapter
(
$chapterId
)
{
if
(
isset
(
$this
->
entityCache
[
'chapters'
])
&&
$this
->
entityCache
[
'chapters'
]
->
has
(
$chapterId
))
{
return
$this
->
entityCache
[
'chapters'
]
->
get
(
$chapterId
);
}
$chapter
=
$this
->
chapter
->
find
(
$chapterId
);
if
(
$chapter
===
null
)
$chapter
=
false
;
if
(
isset
(
$this
->
entityCache
[
'chapters'
]))
{
$this
->
entityCache
[
'chapters'
]
->
put
(
$chapterId
,
$chapter
);
}
return
$chapter
;
}
/**
* Get the roles for the current user;
* @return array|bool
*/
...
...
@@ -76,6 +129,7 @@ class PermissionService
public
function
buildJointPermissions
()
{
$this
->
jointPermission
->
truncate
();
$this
->
readyEntityCache
();
// Get all roles (Should be the most limited dimension)
$roles
=
$this
->
role
->
with
(
'permissions'
)
->
get
();
...
...
@@ -97,7 +151,7 @@ class PermissionService
}
/**
*
Create
the entity jointPermissions for a particular entity.
*
Rebuild
the entity jointPermissions for a particular entity.
* @param Entity $entity
*/
public
function
buildJointPermissionsForEntity
(
Entity
$entity
)
...
...
@@ -117,6 +171,17 @@ class PermissionService
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
*/
public
function
buildJointPermissionsForEntities
(
Collection
$entities
)
{
$roles
=
$this
->
role
->
with
(
'jointPermissions'
)
->
get
();
$this
->
deleteManyJointPermissionsForEntities
(
$entities
);
$this
->
createManyJointPermissions
(
$entities
,
$roles
);
}
/**
* Build the entity jointPermissions for a particular role.
* @param Role $role
*/
...
...
@@ -177,9 +242,14 @@ class PermissionService
*/
protected
function
deleteManyJointPermissionsForEntities
(
$entities
)
{
$query
=
$this
->
jointPermission
->
newQuery
();
foreach
(
$entities
as
$entity
)
{
$entity
->
jointPermissions
()
->
delete
();
$query
->
orWhere
(
function
(
$query
)
use
(
$entity
)
{
$query
->
where
(
'entity_id'
,
'='
,
$entity
->
id
)
->
where
(
'entity_type'
,
'='
,
$entity
->
getMorphClass
());
});
}
$query
->
delete
();
}
/**
...
...
@@ -189,6 +259,7 @@ class PermissionService
*/
protected
function
createManyJointPermissions
(
$entities
,
$roles
)
{
$this
->
readyEntityCache
();
$jointPermissions
=
[];
foreach
(
$entities
as
$entity
)
{
foreach
(
$roles
as
$role
)
{
...
...
@@ -248,8 +319,9 @@ class PermissionService
}
elseif
(
$entity
->
isA
(
'chapter'
))
{
if
(
!
$entity
->
restricted
)
{
$hasExplicitAccessToBook
=
$entity
->
book
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToBook
=
!
$entity
->
book
->
restricted
;
$book
=
$this
->
getBook
(
$entity
->
book_id
);
$hasExplicitAccessToBook
=
$book
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToBook
=
!
$book
->
restricted
;
return
$this
->
createJointPermissionDataArray
(
$entity
,
$role
,
$action
,
(
$hasExplicitAccessToBook
||
(
$roleHasPermission
&&
$hasPermissiveAccessToBook
)),
(
$hasExplicitAccessToBook
||
(
$roleHasPermissionOwn
&&
$hasPermissiveAccessToBook
)));
...
...
@@ -261,11 +333,14 @@ class PermissionService
}
elseif
(
$entity
->
isA
(
'page'
))
{
if
(
!
$entity
->
restricted
)
{
$hasExplicitAccessToBook
=
$entity
->
book
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToBook
=
!
$entity
->
book
->
restricted
;
$hasExplicitAccessToChapter
=
$entity
->
chapter
&&
$entity
->
chapter
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToChapter
=
$entity
->
chapter
&&
!
$entity
->
chapter
->
restricted
;
$acknowledgeChapter
=
(
$entity
->
chapter
&&
$entity
->
chapter
->
restricted
);
$book
=
$this
->
getBook
(
$entity
->
book_id
);
$hasExplicitAccessToBook
=
$book
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToBook
=
!
$book
->
restricted
;
$chapter
=
$this
->
getChapter
(
$entity
->
chapter_id
);
$hasExplicitAccessToChapter
=
$chapter
&&
$chapter
->
hasActiveRestriction
(
$role
->
id
,
$restrictionAction
);
$hasPermissiveAccessToChapter
=
$chapter
&&
!
$chapter
->
restricted
;
$acknowledgeChapter
=
(
$chapter
&&
$chapter
->
restricted
);
$hasExplicitAccessToParents
=
$acknowledgeChapter
?
$hasExplicitAccessToChapter
:
$hasExplicitAccessToBook
;
$hasPermissiveAccessToParents
=
$acknowledgeChapter
?
$hasPermissiveAccessToChapter
:
$hasPermissiveAccessToBook
;
...
...
app/Services/SocialAuthService.php
View file @
7caed3b
...
...
@@ -158,7 +158,7 @@ class SocialAuthService
$driver
=
trim
(
strtolower
(
$socialDriver
));
if
(
!
in_array
(
$driver
,
$this
->
validSocialDrivers
))
abort
(
404
,
'Social Driver Not Found'
);
if
(
!
$this
->
checkDriverConfigured
(
$driver
))
throw
new
SocialDriverNotConfigured
;
if
(
!
$this
->
checkDriverConfigured
(
$driver
))
throw
new
SocialDriverNotConfigured
(
"Your
{
$driver
}
social settings are not configured correctly."
)
;
return
$driver
;
}
...
...
app/helpers.php
View file @
7caed3b
...
...
@@ -2,33 +2,38 @@
use
BookStack\Ownable
;
if
(
!
function_exists
(
'versioned_asset'
))
{
/**
/**
* Get the path to a versioned file.
*
* @param string $file
* @return string
*
* @throws \InvalidArgumentException
* @throws Exception
*/
function
versioned_asset
(
$file
)
{
static
$manifest
=
null
;
function
versioned_asset
(
$file
=
''
)
{
// Don't require css and JS assets for testing
if
(
config
(
'app.env'
)
===
'testing'
)
return
''
;
if
(
is_null
(
$manifest
))
{
$manifest
=
json_decode
(
file_get_contents
(
public_path
(
'build/manifest.json'
)),
true
);
static
$manifest
=
null
;
$manifestPath
=
'build/manifest.json'
;
if
(
is_null
(
$manifest
)
&&
file_exists
(
$manifestPath
))
{
$manifest
=
json_decode
(
file_get_contents
(
public_path
(
$manifestPath
)),
true
);
}
else
if
(
!
file_exists
(
$manifestPath
))
{
if
(
config
(
'app.env'
)
!==
'production'
)
{
$path
=
public_path
(
$manifestPath
);
$error
=
"No
{
$path
}
file found, Ensure you have built the css/js assets using gulp."
;
}
else
{
$error
=
"No
{
$manifestPath
}
file found, Ensure you are using the release version of BookStack"
;
}
throw
new
\Exception
(
$error
);
}
if
(
isset
(
$manifest
[
$file
]))
{
return
baseUrl
(
$manifest
[
$file
]);
}
if
(
file_exists
(
public_path
(
$file
)))
{
return
baseUrl
(
$file
);
}
throw
new
InvalidArgumentException
(
"File
{
$file
}
not defined in asset manifest."
);
}
}
/**
...
...
database/migrations/2016_07_07_181521_add_summary_to_page_revisions.php
0 → 100644
View file @
7caed3b
<?php
use
Illuminate\Database\Schema\Blueprint
;
use
Illuminate\Database\Migrations\Migration
;
class
AddSummaryToPageRevisions
extends
Migration
{
/**
* Run the migrations.
*
* @return void
*/
public
function
up
()
{
Schema
::
table
(
'page_revisions'
,
function
(
$table
)
{
$table
->
string
(
'summary'
)
->
nullable
();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public
function
down
()
{
Schema
::
table
(
'page_revisions'
,
function
(
$table
)
{
$table
->
dropColumn
(
'summary'
);
});
}
}
phpunit.xml
View file @
7caed3b
...
...
@@ -28,7 +28,7 @@
<env
name=
"DB_CONNECTION"
value=
"mysql_testing"
/>
<env
name=
"MAIL_DRIVER"
value=
"log"
/>
<env
name=
"AUTH_METHOD"
value=
"standard"
/>
<env
name=
"DISABLE_EXTERNAL_SERVICES"
value=
"
fals
e"
/>
<env
name=
"DISABLE_EXTERNAL_SERVICES"
value=
"
tru
e"
/>
<env
name=
"LDAP_VERSION"
value=
"3"
/>
<env
name=
"GITHUB_APP_ID"
value=
"aaaaaaaaaaaaaa"
/>
<env
name=
"GITHUB_APP_SECRET"
value=
"aaaaaaaaaaaaaa"
/>
...
...
resources/assets/js/controllers.js
View file @
7caed3b
...
...
@@ -69,7 +69,7 @@ module.exports = function (ngApp, events) {
*/
function
callbackAndHide
(
returnData
)
{
if
(
callback
)
callback
(
returnData
);
$scope
.
showing
=
false
;
$scope
.
hide
()
;
}
/**
...
...
@@ -109,6 +109,7 @@ module.exports = function (ngApp, events) {
function
show
(
doneCallback
)
{
callback
=
doneCallback
;
$scope
.
showing
=
true
;
$
(
'#image-manager'
).
find
(
'.overlay'
).
css
(
'display'
,
'flex'
).
hide
().
fadeIn
(
240
);
// Get initial images if they have not yet been loaded in.
if
(
!
dataLoaded
)
{
fetchData
();
...
...
@@ -131,6 +132,7 @@ module.exports = function (ngApp, events) {
*/
$scope
.
hide
=
function
()
{
$scope
.
showing
=
false
;
$
(
'#image-manager'
).
find
(
'.overlay'
).
fadeOut
(
240
);
};
var
baseUrl
=
window
.
baseUrl
(
'/images/'
+
$scope
.
imageType
+
'/all/'
);
...
...
@@ -357,8 +359,6 @@ module.exports = function (ngApp, events) {
/**
* Save a draft update into the system via an AJAX request.
* @param title
* @param html
*/
function
saveDraft
()
{
var
data
=
{
...
...
@@ -373,9 +373,17 @@ module.exports = function (ngApp, events) {
var
updateTime
=
moment
.
utc
(
moment
.
unix
(
responseData
.
data
.
timestamp
)).
toDate
();
$scope
.
draftText
=
responseData
.
data
.
message
+
moment
(
updateTime
).
format
(
'HH:mm'
);
if
(
!
$scope
.
isNewPageDraft
)
$scope
.
isUpdateDraft
=
true
;
showDraftSaveNotification
();
});
}
function
showDraftSaveNotification
()
{
$scope
.
draftUpdated
=
true
;
$timeout
(()
=>
{
$scope
.
draftUpdated
=
false
;
},
2000
)
}
$scope
.
forceDraftSave
=
function
()
{
saveDraft
();
};
...
...
resources/assets/js/directives.js
View file @
7caed3b
This diff is collapsed.
Click to expand it.
resources/assets/js/global.js
View file @
7caed3b
...
...
@@ -18,9 +18,12 @@ window.baseUrl = function(path) {
var
ngApp
=
angular
.
module
(
'bookStack'
,
[
'ngResource'
,
'ngAnimate'
,
'ngSanitize'
,
'ui.sortable'
]);
// Global Event System
var
Events
=
{
listeners
:
{},
emit
:
function
(
eventName
,
eventData
)
{
class
EventManager
{
constructor
()
{
this
.
listeners
=
{};
}
emit
(
eventName
,
eventData
)
{
if
(
typeof
this
.
listeners
[
eventName
]
===
'undefined'
)
return
this
;
var
eventsToStart
=
this
.
listeners
[
eventName
];
for
(
let
i
=
0
;
i
<
eventsToStart
.
length
;
i
++
)
{
...
...
@@ -28,33 +31,35 @@ var Events = {
event
(
eventData
);
}
return
this
;
},
listen
:
function
(
eventName
,
callback
)
{
}
listen
(
eventName
,
callback
)
{
if
(
typeof
this
.
listeners
[
eventName
]
===
'undefined'
)
this
.
listeners
[
eventName
]
=
[];
this
.
listeners
[
eventName
].
push
(
callback
);
return
this
;
}
};
window
.
Events
=
Events
;
window
.
Events
=
new
EventManager
()
;
var
services
=
require
(
'./services'
)(
ngApp
,
Events
);
var
directives
=
require
(
'./directives'
)(
ngApp
,
Events
);
var
controllers
=
require
(
'./controllers'
)(
ngApp
,
Events
);
var
services
=
require
(
'./services'
)(
ngApp
,
window
.
Events
);
var
directives
=
require
(
'./directives'
)(
ngApp
,
window
.
Events
);
var
controllers
=
require
(
'./controllers'
)(
ngApp
,
window
.
Events
);
//Global jQuery Config & Extensions
// Smooth scrolling
jQuery
.
fn
.
smoothScrollTo
=
function
()
{
if
(
this
.
length
===
0
)
return
;
$
(
'body'
).
animate
({
let
scrollElem
=
document
.
documentElement
.
scrollTop
===
0
?
document
.
body
:
document
.
documentElement
;
$
(
scrollElem
).
animate
({
scrollTop
:
this
.
offset
().
top
-
60
// Adjust to change final scroll position top margin
},
800
);
// Adjust to change animations speed (ms)
return
this
;
};
// Making contains text expression not worry about casing
$
.
expr
[
":"
].
contains
=
$
.
expr
.
createPseudo
(
function
(
arg
)
{
jQuery
.
expr
[
":"
].
contains
=
$
.
expr
.
createPseudo
(
function
(
arg
)
{
return
function
(
elem
)
{
return
$
(
elem
).
text
().
toUpperCase
().
indexOf
(
arg
.
toUpperCase
())
>=
0
;
};
...
...
@@ -104,13 +109,14 @@ $(function () {
var
scrollTop
=
document
.
getElementById
(
'back-to-top'
);
var
scrollTopBreakpoint
=
1200
;
window
.
addEventListener
(
'scroll'
,
function
()
{
if
(
!
scrollTopShowing
&&
document
.
body
.
scrollTop
>
scrollTopBreakpoint
)
{
let
scrollTopPos
=
document
.
documentElement
.
scrollTop
||
document
.
body
.
scrollTop
||
0
;
if
(
!
scrollTopShowing
&&
scrollTopPos
>
scrollTopBreakpoint
)
{
scrollTop
.
style
.
display
=
'block'
;
scrollTopShowing
=
true
;
setTimeout
(()
=>
{
scrollTop
.
style
.
opacity
=
0.4
;
},
1
);
}
else
if
(
scrollTopShowing
&&
document
.
body
.
scrollTop
<
scrollTopBreakpoint
)
{
}
else
if
(
scrollTopShowing
&&
scrollTopPos
<
scrollTopBreakpoint
)
{
scrollTop
.
style
.
opacity
=
0
;
scrollTopShowing
=
false
;
setTimeout
(()
=>
{
...
...
@@ -124,6 +130,27 @@ $(function () {
$
(
'.entity-list.compact'
).
find
(
'p'
).
not
(
'.empty-text'
).
slideToggle
(
240
);
});
// Popup close
$
(
'.popup-close'
).
click
(
function
()
{
$
(
this
).
closest
(
'.overlay'
).
fadeOut
(
240
);
});
$
(
'.overlay'
).
click
(
function
(
event
)
{
if
(
!
$
(
event
.
target
).
hasClass
(
'overlay'
))
return
;
$
(
this
).
fadeOut
(
240
);
});
// Prevent markdown display link click redirect
$
(
'.markdown-display'
).
on
(
'click'
,
'a'
,
function
(
event
)
{
event
.
preventDefault
();
window
.
open
(
$
(
this
).
attr
(
'href'
));
});
// Detect IE for css
if
(
navigator
.
userAgent
.
indexOf
(
'MSIE'
)
!==-
1
||
navigator
.
appVersion
.
indexOf
(
'Trident/'
)
>
0
||
navigator
.
userAgent
.
indexOf
(
'Safari'
)
!==
-
1
){
$
(
'body'
).
addClass
(
'flexbox-support'
);
}
});
...
...
resources/assets/js/pages/page-form.js
View file @
7caed3b
"use strict"
;
/**
* Handle pasting images from clipboard.
* @param e - event
* @param editor - editor instance
*/
function
editorPaste
(
e
,
editor
)
{
if
(
!
e
.
clipboardData
)
return
let
items
=
e
.
clipboardData
.
items
;
if
(
!
items
)
return
;
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
items
[
i
].
type
.
indexOf
(
"image"
)
===
-
1
)
return
let
file
=
items
[
i
].
getAsFile
();
let
formData
=
new
FormData
();
let
ext
=
'png'
;
let
xhr
=
new
XMLHttpRequest
();
if
(
file
.
name
)
{
let
fileNameMatches
=
file
.
name
.
match
(
/
\.(
.+
)
$/
);
if
(
fileNameMatches
)
{
ext
=
fileNameMatches
[
1
];
}
}
let
id
=
"image-"
+
Math
.
random
().
toString
(
16
).
slice
(
2
);
let
loadingImage
=
window
.
baseUrl
(
'/loading.gif'
);
editor
.
execCommand
(
'mceInsertContent'
,
false
,
`<img src="
${
loadingImage
}
" id="
${
id
}
">`
);
let
remoteFilename
=
"image-"
+
Date
.
now
()
+
"."
+
ext
;
formData
.
append
(
'file'
,
file
,
remoteFilename
);
formData
.
append
(
'_token'
,
document
.
querySelector
(
'meta[name="token"]'
).
getAttribute
(
'content'
));
xhr
.
open
(
'POST'
,
window
.
baseUrl
(
'/images/gallery/upload'
));
xhr
.
onload
=
function
()
{
if
(
xhr
.
status
===
200
||
xhr
.
status
===
201
)
{
let
result
=
JSON
.
parse
(
xhr
.
responseText
);
editor
.
dom
.
setAttrib
(
id
,
'src'
,
result
.
thumbs
.
display
);
}
else
{
console
.
log
(
'An error occurred uploading the image'
,
xhr
.
responseText
);
editor
.
dom
.
remove
(
id
);
}
};
xhr
.
send
(
formData
);
}
}
function
registerEditorShortcuts
(
editor
)
{
// Headers
for
(
let
i
=
1
;
i
<
5
;
i
++
)
{
editor
.
addShortcut
(
'ctrl+'
+
i
,
''
,
[
'FormatBlock'
,
false
,
'h'
+
i
]);
}
// Other block shortcuts
editor
.
addShortcut
(
'ctrl+q'
,
''
,
[
'FormatBlock'
,
false
,
'blockquote'
]);
editor
.
addShortcut
(
'ctrl+d'
,
''
,
[
'FormatBlock'
,
false
,
'p'
]);
editor
.
addShortcut
(
'ctrl+e'
,
''
,
[
'FormatBlock'
,
false
,
'pre'
]);
editor
.
addShortcut
(
'ctrl+s'
,
''
,
[
'FormatBlock'
,
false
,
'code'
]);
}
var
mceOptions
=
module
.
exports
=
{
selector
:
'#html-editor'
,
content_css
:
[
...
...
@@ -6,6 +68,8 @@ var mceOptions = module.exports = {
],
body_class
:
'page-content'
,
relative_urls
:
false
,
remove_script_host
:
false
,
document_base_url
:
window
.
baseUrl
(
'/'
),
statusbar
:
false
,
menubar
:
false
,
paste_data_images
:
false
,
...
...
@@ -38,23 +102,41 @@ var mceOptions = module.exports = {
alignright
:
{
selector
:
'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img'
,
classes
:
'align-right'
},
},
file_browser_callback
:
function
(
field_name
,
url
,
type
,
win
)
{
if
(
type
===
'file'
)
{
window
.
showEntityLinkSelector
(
function
(
entity
)
{
let
originalField
=
win
.
document
.
getElementById
(
field_name
);
originalField
.
value
=
entity
.
link
;
$
(
originalField
).
closest
(
'.mce-form'
).
find
(
'input'
).
eq
(
2
).
val
(
entity
.
name
);
});
}
if
(
type
===
'image'
)
{
// Show image manager
window
.
ImageManager
.
showExternal
(
function
(
image
)
{
// Set popover link input to image url then fire change event
// to ensure the new value sticks
win
.
document
.
getElementById
(
field_name
).
value
=
image
.
url
;
if
(
"createEvent"
in
document
)
{
var
evt
=
document
.
createEvent
(
"HTMLEvents"
);
let
evt
=
document
.
createEvent
(
"HTMLEvents"
);
evt
.
initEvent
(
"change"
,
false
,
true
);
win
.
document
.
getElementById
(
field_name
).
dispatchEvent
(
evt
);
}
else
{
win
.
document
.
getElementById
(
field_name
).
fireEvent
(
"onchange"
);
}
var
html
=
'<a href="'
+
image
.
url
+
'" target="_blank">'
;
html
+=
'<img src="'
+
image
.
thumbs
.
display
+
'" alt="'
+
image
.
name
+
'">'
;
// Replace the actively selected content with the linked image
let
html
=
`<a href="
${
image
.
url
}
" target="_blank">`
;
html
+=
`<img src="
${
image
.
thumbs
.
display
}
" alt="
${
image
.
name
}
">`
;
html
+=
'</a>'
;
win
.
tinyMCE
.
activeEditor
.
execCommand
(
'mceInsertContent'
,
false
,
html
);
});
}
},
paste_preprocess
:
function
(
plugin
,
args
)
{
var
content
=
args
.
content
;
let
content
=
args
.
content
;
if
(
content
.
indexOf
(
'<img src="file://'
)
!==
-
1
)
{
args
.
content
=
''
;
}
...
...
@@ -62,10 +144,14 @@ var mceOptions = module.exports = {
extraSetups
:
[],
setup
:
function
(
editor
)
{
for
(
var
i
=
0
;
i
<
mceOptions
.
extraSetups
.
length
;
i
++
)
{
// Run additional setup actions
// Used by the angular side of things
for
(
let
i
=
0
;
i
<
mceOptions
.
extraSetups
.
length
;
i
++
)
{
mceOptions
.
extraSetups
[
i
](
editor
);
}
registerEditorShortcuts
(
editor
);
(
function
()
{
var
wrap
;
...
...
@@ -76,13 +162,12 @@ var mceOptions = module.exports = {
editor
.
on
(
'dragstart'
,
function
()
{
var
node
=
editor
.
selection
.
getNode
();
if
(
node
.
nodeName
===
'IMG'
)
{
if
(
node
.
nodeName
!==
'IMG'
)
return
;
wrap
=
editor
.
dom
.
getParent
(
node
,
'.mceTemp'
);
if
(
!
wrap
&&
node
.
parentNode
.
nodeName
===
'A'
&&
!
hasTextContent
(
node
.
parentNode
))
{
wrap
=
node
.
parentNode
;
}
}
});
editor
.
on
(
'drop'
,
function
(
event
)
{
...
...
@@ -106,15 +191,15 @@ var mceOptions = module.exports = {
});
})();
// Image picker button
//
Custom
Image picker button
editor
.
addButton
(
'image-insert'
,
{
title
:
'My title'
,
icon
:
'image'
,
tooltip
:
'Insert an image'
,
onclick
:
function
()
{
window
.
ImageManager
.
showExternal
(
function
(
image
)
{
var
html
=
'<a href="'
+
image
.
url
+
'" target="_blank">'
;
html
+=
'<img src="'
+
image
.
thumbs
.
display
+
'" alt="'
+
image
.
name
+
'">'
;
let
html
=
`<a href="
${
image
.
url
}
" target="_blank">`
;
html
+=
`<img src="
${
image
.
thumbs
.
display
}
" alt="
${
image
.
name
}
">`
;
html
+=
'</a>'
;
editor
.
execCommand
(
'mceInsertContent'
,
false
,
html
);
});
...
...
@@ -122,49 +207,8 @@ var mceOptions = module.exports = {
});
// Paste image-uploads
editor
.
on
(
'paste'
,
function
(
e
)
{
if
(
e
.
clipboardData
)
{
var
items
=
e
.
clipboardData
.
items
;
if
(
items
)
{
for
(
var
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
items
[
i
].
type
.
indexOf
(
"image"
)
!==
-
1
)
{
var
file
=
items
[
i
].
getAsFile
();
var
formData
=
new
FormData
();
var
ext
=
'png'
;
var
xhr
=
new
XMLHttpRequest
();
if
(
file
.
name
)
{
var
fileNameMatches
=
file
.
name
.
match
(
/
\.(
.+
)
$/
);
if
(
fileNameMatches
)
{
ext
=
fileNameMatches
[
1
];
}
}
var
id
=
"image-"
+
Math
.
random
().
toString
(
16
).
slice
(
2
);
editor
.
execCommand
(
'mceInsertContent'
,
false
,
'<img src="/loading.gif" id="'
+
id
+
'">'
);
var
remoteFilename
=
"image-"
+
Date
.
now
()
+
"."
+
ext
;
formData
.
append
(
'file'
,
file
,
remoteFilename
);
formData
.
append
(
'_token'
,
document
.
querySelector
(
'meta[name="token"]'
).
getAttribute
(
'content'
));
xhr
.
open
(
'POST'
,
window
.
baseUrl
(
'/images/gallery/upload'
));
xhr
.
onload
=
function
()
{
if
(
xhr
.
status
===
200
||
xhr
.
status
===
201
)
{
var
result
=
JSON
.
parse
(
xhr
.
responseText
);
editor
.
dom
.
setAttrib
(
id
,
'src'
,
result
.
url
);
}
else
{
console
.
log
(
'An error occured uploading the image'
);
console
.
log
(
xhr
.
responseText
);
editor
.
dom
.
remove
(
id
);
}
};
xhr
.
send
(
formData
);
}
}
}
}
editor
.
on
(
'paste'
,
function
(
event
)
{
editorPaste
(
event
,
editor
);
});
}
};
\ No newline at end of file
...
...
resources/assets/sass/_buttons.scss
View file @
7caed3b
...
...
@@ -100,3 +100,13 @@ $button-border-radius: 2px;
}
}
.button
[
disabled
]
{
background-color
:
#BBB
;
cursor
:
default
;
&
:hover
{
background-color
:
#BBB
;
cursor
:
default
;
box-shadow
:
none
;
}
}
...
...
resources/assets/sass/_
image-manager
.scss
→
resources/assets/sass/_
components
.scss
View file @
7caed3b
.overlay
{
background-color
:
rgba
(
0
,
0
,
0
,
0
.
2
);
background-color
:
rgba
(
0
,
0
,
0
,
0
.
333
);
position
:
fixed
;
z-index
:
95536
;
width
:
100%
;
...
...
@@ -10,37 +10,81 @@
left
:
0
;
right
:
0
;
bottom
:
0
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
display
:
none
;
}
.image-manager-body
{
.popup-body-wrap
{
display
:
flex
;
}
.popup-body
{
background-color
:
#FFF
;
max-height
:
90%
;
width
:
90%
;
height
:
90%
;
width
:
1200px
;
height
:
auto
;
margin
:
2%
5%
;
border-radius
:
4px
;
box-shadow
:
0
0
15px
0
rgba
(
0
,
0
,
0
,
0
.3
);
overflow
:
hidden
;
position
:
fixed
;
top
:
0
;
bottom
:
0
;
left
:
0
;
z-index
:
999
;
display
:
flex
;
h1
,
h2
,
h3
{
font-weight
:
300
;
flex-direction
:
column
;
&
.small
{
margin
:
2%
auto
;
width
:
800px
;
max-width
:
90%
;
}
&
:before
{
display
:
flex
;
align-self
:
flex-start
;
}
}
#image-manager
.dropzone-container
{
position
:
relative
;
border
:
3px
dashed
#DDD
;
}
//body.ie .popup-body {
// min-height: 100%;
//}
.
image-manager-bottom
{
.
corner-button
{
position
:
absolute
;
bottom
:
0
;
top
:
0
;
right
:
0
;
margin
:
0
;
height
:
40px
;
border-radius
:
0
;
box-shadow
:
none
;
}
.popup-header
,
.popup-footer
{
display
:
block
!
important
;
position
:
relative
;
height
:
40px
;
flex
:
none
!
important
;
.popup-title
{
color
:
#FFF
;
padding
:
8px
$-m
;
}
}
body
.flexbox-support
#entity-selector-wrap
.popup-body
.form-group
{
height
:
444px
;
min-height
:
444px
;
}
#entity-selector-wrap
.popup-body
.form-group
{
margin
:
0
;
}
//body.ie #entity-selector-wrap .popup-body .form-group {
// min-height: 60vh;
//}
.image-manager-body
{
min-height
:
70vh
;
}
#image-manager
.dropzone-container
{
position
:
relative
;
border
:
3px
dashed
#DDD
;
}
.image-manager-list
.image
{
...
...
@@ -103,18 +147,13 @@
.image-manager-sidebar
{
width
:
300px
;
height
:
100%
;
margin-left
:
1px
;
padding
:
0
$-l
;
padding
:
$-m
$-l
;
overflow-y
:
auto
;
border-left
:
1px
solid
#DDD
;
}
.image-manager-close
{
position
:
absolute
;
top
:
0
;
right
:
0
;
margin
:
0
;
border-radius
:
0
;
.dropzone-container
{
margin-top
:
$-m
;
}
}
.image-manager-list
{
...
...
@@ -125,7 +164,6 @@
.image-manager-content
{
display
:
flex
;
flex-direction
:
column
;
height
:
100%
;
flex
:
1
;
.container
{
width
:
100%
;
...
...
@@ -141,12 +179,13 @@
* Copyright (c) 2012 Matias Meno <m@tias.me>
*/
.dz-message
{
font-size
:
1
.4em
;
font-size
:
1
.2em
;
line-height
:
1
.1
;
font-style
:
italic
;
color
:
#aaa
;
text-align
:
center
;
cursor
:
pointer
;
padding
:
$-
x
l
$-m
;
padding
:
$-l
$-m
;
transition
:
all
ease-in-out
120ms
;
}
...
...
resources/assets/sass/_grid.scss
View file @
7caed3b
...
...
@@ -25,6 +25,14 @@ body.flexbox {
}
}
.flex-child
>
div
{
flex
:
1
;
}
//body.ie .flex-child > div {
// flex: 1 0 0px;
//}
/** Rules for all columns */
div
[
class
^=
"col-"
]
img
{
max-width
:
100%
;
...
...
@@ -39,6 +47,9 @@ div[class^="col-"] img {
&
.fluid
{
max-width
:
100%
;
}
&
.medium
{
max-width
:
992px
;
}
&
.small
{
max-width
:
840px
;
}
...
...
resources/assets/sass/_header.scss
View file @
7caed3b
...
...
@@ -155,6 +155,7 @@ form.search-box {
text-decoration
:
none
;
}
}
}
.faded
span
.faded-text
{
...
...
resources/assets/sass/_lists.scss
View file @
7caed3b
...
...
@@ -375,6 +375,9 @@ ul.pagination {
.text-muted
{
color
:
#999
;
}
li
.padded
{
padding
:
$-xs
$-m
;
}
a
{
display
:
block
;
padding
:
$-xs
$-m
;
...
...
@@ -384,10 +387,10 @@ ul.pagination {
background-color
:
#EEE
;
}
i
{
margin-right
:
$-
m
;
margin-right
:
$-
s
;
padding-right
:
0
;
display
:
inline
;
width
:
22
px
;
display
:
inline
-block
;
width
:
16
px
;
}
}
li
.border-bottom
{
...
...
resources/assets/sass/_pages.scss
View file @
7caed3b
...
...
@@ -20,6 +20,16 @@
}
}
.draft-notification
{
pointer-events
:
none
;
transform
:
scale
(
0
);
transition
:
transform
ease-in-out
120ms
;
transform-origin
:
50%
50%
;
&
.visible
{
transform
:
scale
(
1
);
}
}
.page-style.editor
{
padding
:
0
!
important
;
}
...
...
@@ -238,7 +248,7 @@
}
.tag-display
{
margin
:
$-xl
$-
xs
;
margin
:
$-xl
$-
m
;
border
:
1px
solid
#DDD
;
min-width
:
180px
;
max-width
:
320px
;
...
...
resources/assets/sass/styles.scss
View file @
7caed3b
...
...
@@ -12,7 +12,7 @@
@import
"animations"
;
@import
"tinymce"
;
@import
"highlightjs"
;
@import
"
image-manager
"
;
@import
"
components
"
;
@import
"header"
;
@import
"lists"
;
@import
"pages"
;
...
...
@@ -72,7 +72,7 @@ body.dragging, body.dragging * {
border-radius
:
3px
;
box-shadow
:
$bs-med
;
z-index
:
999999
;
display
:
table
;
display
:
block
;
cursor
:
pointer
;
max-width
:
480px
;
i
,
span
{
...
...
resources/views/books/list-item.blade.php
View file @
7caed3b
<div
class=
"book entity-list-item"
data-entity-type=
"book"
data-entity-id=
"{{$book->id}}"
>
<h3
class=
"text-book"
><a
class=
"text-book
"
href=
"{{$book->getUrl()}}"
><i
class=
"zmdi zmdi-book"
></i>
{{$book->name}}
</a></h3>
<h3
class=
"text-book"
><a
class=
"text-book
entity-list-item-link"
href=
"{{$book->getUrl()}}"
><i
class=
"zmdi zmdi-book"
></i><span
class=
"entity-list-item-name"
>
{{$book->name}}
</span>
</a></h3>
@if(isset($book->searchSnippet))
<p
class=
"text-muted"
>
{!! $book->searchSnippet !!}
</p>
@else
...
...
resources/views/books/sort.blade.php
View file @
7caed3b
...
...
@@ -50,7 +50,7 @@
var
sortableOptions
=
{
group
:
'serialization'
,
onDrop
:
function
(
$item
,
container
,
_super
)
{
var
pageMap
=
build
Page
Map
();
var
pageMap
=
build
Entity
Map
();
$
(
'#sort-tree-input'
).
val
(
JSON
.
stringify
(
pageMap
));
_super
(
$item
,
container
);
},
...
...
@@ -74,29 +74,42 @@
$link
.
remove
();
});
function
buildPageMap
()
{
var
pageMap
=
[];
/**
* Build up a mapping of entities with their ordering and nesting.
* @returns {Array}
*/
function
buildEntityMap
()
{
var
entityMap
=
[];
var
$lists
=
$
(
'.sort-list'
);
$lists
.
each
(
function
(
listIndex
)
{
var
list
=
$
(
this
);
var
bookId
=
list
.
closest
(
'[data-type="book"]'
).
attr
(
'data-id'
);
var
$
childElements
=
list
.
find
(
'[data-type="page"],
[data-type="chapter"]'
);
$
childElements
.
each
(
function
(
c
hildIndex
)
{
var
$
directChildren
=
list
.
find
(
'> [data-type="page"], >
[data-type="chapter"]'
);
$
directChildren
.
each
(
function
(
directC
hildIndex
)
{
var
$childElem
=
$
(
this
);
var
type
=
$childElem
.
attr
(
'data-type'
);
var
parentChapter
=
false
;
if
(
type
===
'page'
&&
$childElem
.
closest
(
'[data-type="chapter"]'
).
length
===
1
)
{
parentChapter
=
$childElem
.
closest
(
'[data-type="chapter"]'
).
attr
(
'data-id'
);
}
pageMap
.
push
({
id
:
$childElem
.
attr
(
'data-id'
),
var
childId
=
$childElem
.
attr
(
'data-id'
);
entityMap
.
push
({
id
:
childId
,
sort
:
directChildIndex
,
parentChapter
:
parentChapter
,
type
:
type
,
book
:
bookId
});
$chapterChildren
=
$childElem
.
find
(
'[data-type="page"]'
).
each
(
function
(
pageIndex
)
{
var
$chapterChild
=
$
(
this
);
entityMap
.
push
({
id
:
$chapterChild
.
attr
(
'data-id'
),
sort
:
pageIndex
,
parentChapter
:
childId
,
type
:
'page'
,
book
:
bookId
});
});
});
});
return
page
Map
;
return
entity
Map
;
}
});
...
...
resources/views/chapters/list-item.blade.php
View file @
7caed3b
...
...
@@ -6,8 +6,8 @@
</a>
<span
class=
"text-muted"
>
»
</span>
@endif
<a
href=
"{{ $chapter->getUrl() }}"
class=
"text-chapter"
>
<i
class=
"zmdi zmdi-collection-bookmark"
></i>
{{ $chapter->name }}
<a
href=
"{{ $chapter->getUrl() }}"
class=
"text-chapter
entity-list-item-link
"
>
<i
class=
"zmdi zmdi-collection-bookmark"
></i>
<span
class=
"entity-list-item-name"
>
{{ $chapter->name }}
</span>
</a>
</h3>
@if(isset($chapter->searchSnippet))
...
...
resources/views/pages/edit.blade.php
View file @
7caed3b
...
...
@@ -19,6 +19,14 @@
</div>
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
@include('partials/entity-selector-popup')
<script>
(
function
()
{
})();
</script>
@stop
\ No newline at end of file
...
...
resources/views/pages/form.blade.php
View file @
7caed3b
...
...
@@ -13,8 +13,9 @@
</div>
<div
class=
"col-sm-4 faded text-center"
>
<div
dropdown
class=
"dropdown-container"
>
<div
dropdown
class=
"dropdown-container
draft-display
"
>
<a
dropdown-toggle
class=
"text-primary text-button"
><span
class=
"faded-text"
ng-bind=
"draftText"
></span>
<i
class=
"zmdi zmdi-more-vert"
></i></a>
<i
class=
"zmdi zmdi-check-circle text-pos draft-notification"
ng-class=
"{visible: draftUpdated}"
></i>
<ul>
<li>
<a
ng-click=
"forceDraftSave()"
class=
"text-pos"
><i
class=
"zmdi zmdi-save"
></i>
Save Draft
</a>
...
...
@@ -22,13 +23,24 @@
<li
ng-if=
"isNewPageDraft"
>
<a
href=
"{{ $model->getUrl('/delete') }}"
class=
"text-neg"
><i
class=
"zmdi zmdi-delete"
></i>
Delete Draft
</a>
</li>
<li>
<a
type=
"button"
ng-if=
"isUpdateDraft"
ng-click=
"discardDraft()"
class=
"text-neg"
><i
class=
"zmdi zmdi-close-circle"
></i>
Discard Draft
</a>
</li>
</ul>
</div>
</div>
<div
class=
"col-sm-4 faded"
>
<div
class=
"action-buttons"
ng-cloak
>
<div
dropdown
class=
"dropdown-container"
>
<a
dropdown-toggle
class=
"text-primary text-button"
><i
class=
"zmdi zmdi-edit"
></i>
@{{(changeSummary | limitTo:16) + (changeSummary.length>16?'...':'') || 'Set Changelog'}}
</a>
<ul
class=
"wide"
>
<li
class=
"padded"
>
<p
class=
"text-muted"
>
Enter a brief description of the changes you've made
</p>
<input
name=
"summary"
id=
"summary-input"
type=
"text"
placeholder=
"Enter Changelog"
ng-model=
"changeSummary"
/>
</li>
</ul>
</div>
<button
type=
"button"
ng-if=
"isUpdateDraft"
ng-click=
"discardDraft()"
class=
"text-button text-neg"
><i
class=
"zmdi zmdi-close-circle"
></i>
Discard Draft
</button>
<button
type=
"submit"
id=
"save-button"
class=
"text-button text-pos"
><i
class=
"zmdi zmdi-floppy"
></i>
Save Page
</button>
</div>
</div>
...
...
@@ -62,6 +74,8 @@
<span
class=
"float left"
>
Editor
</span>
<div
class=
"float right buttons"
>
<button
class=
"text-button"
type=
"button"
data-action=
"insertImage"
><i
class=
"zmdi zmdi-image"
></i>
Insert Image
</button>
|
<button
class=
"text-button"
type=
"button"
data-action=
"insertEntityLink"
><i
class=
"zmdi zmdi-link"
></i>
Insert Entity Link
</button>
</div>
</div>
...
...
resources/views/pages/list-item.blade.php
View file @
7caed3b
<div
class=
"page {{$page->draft ? 'draft' : ''}} entity-list-item"
data-entity-type=
"page"
data-entity-id=
"{{$page->id}}"
>
<h3>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-page
"
><i
class=
"zmdi zmdi-file-text"
></i>
{{ $page->name }}
</a>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-page
entity-list-item-link"
><i
class=
"zmdi zmdi-file-text"
></i><span
class=
"entity-list-item-name"
>
{{ $page->name }}
</span>
</a>
</h3>
@if(isset($page->searchSnippet))
...
...
resources/views/pages/restrictions.blade.php
View file @
7caed3b
...
...
@@ -16,7 +16,7 @@
</a>
@endif
<span
class=
"sep"
>
»
</span>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-
book
text-button"
><i
class=
"zmdi zmdi-file"
></i>
{{ $page->getShortName() }}
</a>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-
page
text-button"
><i
class=
"zmdi zmdi-file"
></i>
{{ $page->getShortName() }}
</a>
</div>
</div>
</div>
...
...
resources/views/pages/revisions.blade.php
View file @
7caed3b
...
...
@@ -5,45 +5,59 @@
<div
class=
"faded-small toolbar"
>
<div
class=
"container"
>
<div
class=
"row"
>
<div
class=
"col-
md-6
faded"
>
<div
class=
"col-
sm-12
faded"
>
<div
class=
"breadcrumbs"
>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-primary text-button"
><i
class=
"zmdi zmdi-arrow-left"
></i>
Back to page
</a>
</div>
<a
href=
"{{ $page->book->getUrl() }}"
class=
"text-book text-button"
><i
class=
"zmdi zmdi-book"
></i>
{{ $page->book->getShortName() }}
</a>
@if($page->hasChapter())
<span
class=
"sep"
>
»
</span>
<a
href=
"{{ $page->chapter->getUrl() }}"
class=
"text-chapter text-button"
>
<i
class=
"zmdi zmdi-collection-bookmark"
></i>
{{ $page->chapter->getShortName() }}
</a>
@endif
<span
class=
"sep"
>
»
</span>
<a
href=
"{{ $page->getUrl() }}"
class=
"text-page text-button"
><i
class=
"zmdi zmdi-file"
></i>
{{ $page->getShortName() }}
</a>
</div>
<div
class=
"col-md-6 faded"
>
</div>
</div>
</div>
</div>
<div
class=
"container small"
ng-non-bindable
>
<div
class=
"container"
ng-non-bindable
>
<h1>
Page Revisions
<span
class=
"subheader"
>
For "{{ $page->name }}"
</span></h1>
@if(count($page->revisions) > 0)
<table
class=
"table"
>
<tr>
<th
width=
"40%"
>
Name
</th>
<th
colspan=
"2"
width=
"20%"
>
Created By
</th>
<th
width=
"20%"
>
Revision Date
</th>
<th
width=
"20%"
>
Actions
</th>
<th
width=
"25%"
>
Name
</th>
<th
colspan=
"2"
width=
"10%"
>
Created By
</th>
<th
width=
"15%"
>
Revision Date
</th>
<th
width=
"25%"
>
Changelog
</th>
<th
width=
"15%"
>
Actions
</th>
</tr>
@foreach($page->revisions as $revision)
@foreach($page->revisions as $
index => $
revision)
<tr>
<td>
{{
$revision->name
}}
</td>
<td>
{{
$revision->name
}}
</td>
<td
style=
"line-height: 0;"
>
@if($revision->createdBy)
<img
class=
"avatar"
src=
"{{ $revision->createdBy->getAvatar(30) }}"
alt=
"{{
$revision->createdBy->name
}}"
>
<img
class=
"avatar"
src=
"{{ $revision->createdBy->getAvatar(30) }}"
alt=
"{{
$revision->createdBy->name
}}"
>
@endif
</td>
<td>
@if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif
</td>
<td><small>
{{$revision->created_at->format('jS F, Y H:i:s')}}
<br>
({{$revision->created_at->diffForHumans()}})
</small></td>
<td>
@if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif
</td>
<td><small>
{{ $revision->created_at->format('jS F, Y H:i:s') }}
<br>
({{ $revision->created_at->diffForHumans() }})
</small></td>
<td>
{{ $revision->summary }}
</td>
@if ($index !== 0)
<td>
<a
href=
"{{ $revision->getUrl() }}"
target=
"_blank"
>
Preview
</a>
<span
class=
"text-muted"
>
|
</span>
<a
href=
"{{ $revision->getUrl('/restore') }}
"
>
Restore
</a>
<a
href=
"{{ $revision->getUrl() }}/restore
"
>
Restore
</a>
</td>
@else
<td><a
target=
"_blank"
href=
"{{ $page->getUrl() }}"
><i>
Current Version
</i></a></td>
@endif
</tr>
@endforeach
</table>
...
...
resources/views/pages/show.blade.php
View file @
7caed3b
...
...
@@ -58,7 +58,7 @@
<div
class=
"container"
id=
"page-show"
ng-non-bindable
>
<div
class=
"row"
>
<div
class=
"col-md-9 print-full-width"
>
<div
class=
"page-content
anim fadeIn
"
>
<div
class=
"page-content"
>
<div
class=
"pointer-container"
id=
"pointer"
>
<div
class=
"pointer anim"
>
...
...
resources/views/partials/entity-selector-popup.blade.php
0 → 100644
View file @
7caed3b
<div
id=
"entity-selector-wrap"
>
<div
class=
"overlay"
entity-link-selector
>
<div
class=
"popup-body small flex-child"
>
<div
class=
"popup-header primary-background"
>
<div
class=
"popup-title"
>
Entity Select
</div>
<button
type=
"button"
class=
"corner-button neg button popup-close"
>
x
</button>
</div>
@include('partials/entity-selector', ['name' => 'entity-selector'])
<div
class=
"popup-footer"
>
<button
type=
"button"
disabled=
"true"
class=
"button entity-link-selector-confirm pos corner-button"
>
Select
</button>
</div>
</div>
</div>
</div>
\ No newline at end of file
resources/views/partials/image-manager.blade.php
View file @
7caed3b
<div
id=
"image-manager"
image-type=
"{{ $imageType }}"
ng-controller=
"ImageManagerController"
uploaded-to=
"{{ $uploaded_to or 0 }}"
>
<div
class=
"overlay anim-slide"
ng-show=
"showing"
ng-cloak
ng-click=
"hide()"
>
<div
class=
"image-manager-body"
ng-click=
"$event.stopPropagation()"
>
<div
class=
"overlay"
ng-cloak
ng-click=
"hide()"
>
<div
class=
"popup-body"
ng-click=
"$event.stopPropagation()"
>
<div
class=
"popup-header primary-background"
>
<div
class=
"popup-title"
>
Image Select
</div>
<button
class=
"popup-close neg corner-button button"
>
x
</button>
</div>
<div
class=
"flex-fill image-manager-body"
>
<div
class=
"image-manager-content"
>
<div
ng-if=
"imageType === 'gallery'"
class=
"container"
>
...
...
@@ -24,7 +31,7 @@
<img
ng-src=
"@{{image.thumbs.gallery}}"
ng-attr-alt=
"@{{image.title}}"
ng-attr-title=
"@{{image.name}}"
>
<div
class=
"image-meta"
>
<span
class=
"name"
ng-bind=
"image.name"
></span>
<span
class=
"date"
>
Uploaded @{{ getDate(image.created_at) | date:'mediumDate'
}}
</span>
<span
class=
"date"
>
Uploaded @{{ getDate(image.created_at)
}}
</span>
</div>
</div>
</div>
...
...
@@ -32,14 +39,10 @@
</div>
</div>
<button
class=
"neg button image-manager-close"
ng-click=
"hide()"
>
x
</button>
<div
class=
"image-manager-sidebar"
>
<h2>
Images
</h2>
<drop-zone
upload-url=
"@{{getUploadUrl()}}"
uploaded-to=
"@{{uploadedTo}}"
event-success=
"uploadSuccess"
></drop-zone>
<div
class=
"image-manager-details anim fadeIn"
ng-show=
"selectedImage"
>
<div
class=
"inner"
>
<hr
class=
"even
"
>
<div
class=
"image-manager-details anim fadeIn"
ng-show=
"selectedImage
"
>
<form
ng-submit=
"saveImageDetails($event)"
>
<div>
...
...
@@ -53,8 +56,6 @@
</div>
</form>
<hr
class=
"even"
>
<div
ng-show=
"dependantPages"
>
<p
class=
"text-neg text-small"
>
This image is used in the pages below, Click delete again to confirm you want to delete
...
...
@@ -67,18 +68,27 @@
</ul>
</div>
<form
ng-submit=
"deleteImage($event)"
>
<button
class=
"button neg"
><i
class=
"zmdi zmdi-delete"
></i>
Delete Image
</button>
<div
class=
"clearfix"
>
<form
class=
"float left"
ng-submit=
"deleteImage($event)"
>
<button
class=
"button icon neg"
><i
class=
"zmdi zmdi-delete"
></i></button>
</form>
</div>
<div
class=
"image-manager-bottom"
>
<button
class=
"button pos anim fadeIn"
ng-show=
"selectedImage"
ng-click=
"selectButtonClick()"
>
<button
class=
"button pos anim fadeIn float right"
ng-show=
"selectedImage"
ng-click=
"selectButtonClick()"
>
<i
class=
"zmdi zmdi-square-right"
></i>
Select Image
</button>
</div>
</div>
<drop-zone
upload-url=
"@{{getUploadUrl()}}"
uploaded-to=
"@{{uploadedTo}}"
event-success=
"uploadSuccess"
></drop-zone>
</div>
</div>
</div>
</div>
</div>
</div>
\ No newline at end of file
...
...
tests/Entity/EntitySearchTest.php
View file @
7caed3b
...
...
@@ -76,6 +76,14 @@ class EntitySearchTest extends TestCase
->
see
(
'Chapter Search Results'
)
->
seeInElement
(
'.entity-list'
,
$chapter
->
name
);
}
public
function
test_search_quote_term_preparation
()
{
$termString
=
'"192" cat "dog hat"'
;
$repo
=
$this
->
app
[
\BookStack\Repos\EntityRepo
::
class
];
$preparedTerms
=
$repo
->
prepareSearchTerms
(
$termString
);
$this
->
assertTrue
(
$preparedTerms
===
[
'"192"'
,
'"dog hat"'
,
'cat'
]);
}
public
function
test_books_search_listing
()
{
$book
=
\BookStack\Book
::
all
()
->
last
();
...
...
tests/Entity/EntityTest.php
View file @
7caed3b
...
...
@@ -218,13 +218,24 @@ class EntityTest extends TestCase
public
function
test_old_page_slugs_redirect_to_new_pages
()
{
$page
=
\BookStack\Page
::
all
()
->
first
();
$page
=
\BookStack\Page
::
first
();
$pageUrl
=
$page
->
getUrl
();
$newPageUrl
=
'/books/'
.
$page
->
book
->
slug
.
'/page/super-test-page'
;
// Need to save twice since revisions are not generated in seeder.
$this
->
asAdmin
()
->
visit
(
$pageUrl
)
->
clickInElement
(
'#content'
,
'Edit'
)
->
type
(
'super test'
,
'#name'
)
->
press
(
'Save Page'
);
$page
=
\BookStack\Page
::
first
();
$pageUrl
=
$page
->
getUrl
();
// Second Save
$this
->
visit
(
$pageUrl
)
->
clickInElement
(
'#content'
,
'Edit'
)
->
type
(
'super test page'
,
'#name'
)
->
press
(
'Save Page'
)
// Check redirect
->
seePageIs
(
$newPageUrl
)
->
visit
(
$pageUrl
)
->
seePageIs
(
$newPageUrl
);
...
...
Please
register
or
sign in
to post a comment