Compare commits

...

238 Commits

Author SHA1 Message Date
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
43122f86e8 Merge pull request #1026 from vriic/master
Update german translation
2018-09-24 10:35:22 +01:00
Nikolai Nikolajevic
935337862c Update german translation 2018-09-24 09:55:37 +02:00
Dan Brown
dd671524f8 Merge pull request #1025 from moucho/master
Spanish translation
2018-09-23 17:18:41 +01:00
Marcos
1cac3d43d3 Spanish translation 2018-09-23 17:31:41 +02:00
Dan Brown
9243c635f2 Made search test a little more consistent 2018-09-23 15:15:44 +01:00
Dan Brown
93f820d9da Updated TinyMCE config to end containers on empty blocks
Makes it easier to escape out of blockquote sections.
Fixes #961
2018-09-23 14:07:50 +01:00
Dan Brown
b62afcad1f Removed search indexing from migration path to prevent Bookshelf issue 2018-09-23 13:25:12 +01:00
Dan Brown
7b32aa163f Added Bookshelves to search system.
Also cleaned up and made search indexing system a little more efficient.
Closes #1023
2018-09-23 12:34:30 +01:00
Dan Brown
eebfd8904e Removed old fulltext indexes from migrations
Prevents forcing of MyISAM for some databases
Removed old code to add indexes and added checks for existing indexes before removal.
Should still allow upgrades, rollbacks to old bookstack versions may be funky but
should not be high use-case.
2018-09-23 00:30:48 +01:00
Dan Brown
0d84a0b976 Extracted search sidebar text to translation file
Closes #864
2018-09-22 23:40:04 +01:00
Dan Brown
88ac636c00 Made it possible to scroll past the end of the markdown editor
Closes #1020
2018-09-22 23:24:51 +01:00
Dan Brown
9dc26a8c52 Prevented TinyMCE clear-color option having scrollbar
Was covering the option if showing and prevented the option being pressed.
Fixes #999
2018-09-22 23:12:39 +01:00
Dan Brown
6543020fd2 Updated tinymce to v4.8.3 2018-09-22 22:58:06 +01:00
Dan Brown
be4f3d62cd Merge branch 'fix/ru-locale' of git://github.com/mullinsmikey/BookStack into mullinsmikey-fix/ru-locale 2018-09-22 22:29:03 +01:00
Dan Brown
da58c41ab6 Prevented attachDefaultRole from trying to re-attach if already existing
Fixes #1003
Added test to cover
2018-09-22 22:09:34 +01:00
Dan Brown
10d08e641c Merge pull request #1021 from moucho/master
Spanish translation
2018-09-22 21:43:19 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Marcos
c4e4cebf14 Spanish translation 2018-09-22 18:36:52 +02:00
Dan Brown
3f58800ed1 Added ability to configure revision limit 2018-09-22 17:30:42 +01:00
Dan Brown
0931ff38e9 Tried to make chapter toggles a little smoother in FF 2018-09-22 16:36:35 +01:00
Dan Brown
07bc0612c0 Merge branch 'master' into fix/#960 2018-09-22 15:57:53 +01:00
Dan Brown
6dec485b45 Fixed sidebar rubber-banding when content is expanded
Hopefully for #905
Ideally layout needs to be re-thought
2018-09-22 15:52:09 +01:00
Dan Brown
a441faf65c Only show codeblock copy icon on hover
Fixes #980
2018-09-22 14:55:33 +01:00
Dan Brown
50ee1462ad Merge pull request #986 from DeehSlash/fix/pt_br_locale
Adds and fixes pt_BR strings
2018-09-22 14:40:23 +01:00
Dan Brown
1cb6ae39c8 Added base RTL support
For #939

- Adds way to check if current language is RTL via config system.
- Made TinyMCE default direction be based on current language text
direction.
- Fixed bullet points to be RTL compatible.
- Set page content body to have direction based on content.
2018-09-22 13:18:26 +01:00
Dan Brown
c667c6e235 Merge branch 'master' of git://github.com/kmoj86/BookStack into kmoj86-master 2018-09-22 12:23:17 +01:00
Dan Brown
e3e484e561 Added custom head content to exports
Closes #981

Also fixed incorrect download tests.
2018-09-22 11:53:40 +01:00
Dan Brown
73fa18a128 Made bookstack cookie name configurable
Closes #1018
2018-09-22 11:42:02 +01:00
Dan Brown
5c2e3f4e56 Extracted download response logic into controller method
Fixes incorrect 'Content-Disposition' header value.
Fixes #581
2018-09-22 11:34:09 +01:00
Dan Brown
c47b578599 Fixed formatting via phpcbf 2018-09-21 18:48:47 +01:00
Dan Brown
e60d11ee04 Altered social auto-reg to be configurable per service
- Added {$service}_AUTO_REGISTER and {$service}_AUTO_CONFIRM_EMAIL env
options for each social auth system.
- Auto-register will allow registration from login, even if registration
is disabled.
- Auto-confirm-email indicates trust and will mark new registrants as
'email_confirmed' and skip 'confirmation email' flow.
- Also added covering tests.
2018-09-21 18:05:06 +01:00
Dan Brown
7ad8314bd7 Merge branch 'feature/autoregistration_social_login' of git://github.com/ibrahimennafaa/BookStack into ibrahimennafaa-feature/autoregistration_social_login 2018-09-21 16:14:52 +01:00
Dan Brown
131fcae4c7 Merge pull request #947 from BookStackApp/bookshelves
Bookshelves
2018-09-21 15:29:52 +01:00
Dan Brown
c8d893fac7 Updated 404 test to not fail based on random long name 2018-09-21 15:24:29 +01:00
Dan Brown
b59e5942c8 Added testing coverage for Bookshelves
Created modified TestResponse so we can use DOM operations in new
Testcases as we move away from the BrowserKit tests.
2018-09-21 15:15:16 +01:00
Dan Brown
8ff969dd17 Updated so permission effect admins more
Asset permissions can now be configured for admins.
joint_permissions will now effect admins more often.
Made so shelves header link will hide if you have no bookshelves view
permission.
2018-09-20 19:48:08 +01:00
Dan Brown
6eead437d8 Added bookshelf permission control UI and copy-down ability 2018-09-20 19:16:11 +01:00
Dan Brown
0b6f83837b Removed joint_permission generation in older migration 2018-09-20 16:03:01 +01:00
Dan Brown
81eb642f75 Added bookshelves homepage options
- Updated homepage selection UI to be more scalable
- Cleaned homepage selection logic in code
- Added seed test data for bookshelves
- Added bookshelves to permission system
2018-09-20 15:27:30 +01:00
Dan Brown
47b08888ba Added bookshelf view, update, delete
- Enabled proper ordering of Books in a shelf.
- Improved related item destroy for all entities.
2018-09-16 19:34:09 +01:00
Abijeet Patro
32e34f10ff Merge pull request #1008 from BookStackApp/revision-deletion
#784 - Adds ability to remove particular revision.
2018-09-16 21:30:04 +05:30
Dan Brown
f455b317ec Added ability to click books in shelf-sort 2018-09-16 16:59:01 +01:00
Abijeet
08b967607f Changes as per code review, and fixes failing test cases.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-16 20:44:09 +05:30
Abijeet
90883bb22b Fixes issue wth the dropdown list upon double clicking.
Closes #960

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-16 13:09:21 +05:30
Abijeet
0c8b6b7324 Final tweaks after code review and fixing failing test cases. 2018-09-16 01:12:36 +05:30
Abijeet
81d3bdc168 Removes the BadRequestException class added earlier.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 21:08:00 +05:30
Abijeet
54ca4487fa Adds tests and few fixes.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 21:05:51 +05:30
Abijeet
25da4d9a8b Added a success message on deletion of revision.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 16:08:20 +05:30
Abijeet
f0add69b61 Adding languages for the revision deletion. 2018-09-15 15:16:04 +05:30
Abijeet
714c7bbd3a Adds code to delete the revision.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-09-15 15:15:42 +05:30
Michael Mullins
e0b479efef UTF-8 slugs & UI fixes 2018-09-11 22:42:25 +04:00
Michael Mullins
9d09c4c7b0 Ru locale fix
Fixed russian translations, added several missing strings
2018-09-10 22:28:47 +04:00
Dan Brown
b89411c108 Copied book content, Added create routes
Added view control
Added pivot table for books relation
Added control to assign books
2018-08-27 14:18:09 +01:00
André Luiz da Silva
c472b82ee3 Adds and fixes pt_BR strings 2018-08-24 16:03:04 -03:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Ibrahim Ennafaa
d2f5313f92 add missing @param in method comment 2018-08-21 12:44:42 -04:00
Ibrahim Ennafaa
572e75b783 Update UserRepo.php 2018-08-20 21:19:25 -04:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
d2a9b312e9 Fixed LDAP group sync using wrong user filter
LDAP group sync was trying to find users based on the external_auth_id
which is not garunteed to match the username entered so somtimes
the search for a user would fail.

This passes the username to the group sync.
Picked up by @yoyokko in #959.
2018-08-19 15:24:42 +01:00
Ibrahim Ennafaa
b224a2c8a0 attempt to fix unit test error for admin creation 2018-08-16 21:52:16 +00:00
Dan Brown
fe6dfcedf9 implement social auto registration feature 2018-08-16 21:26:54 +00:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
01260d95f3 Merge pull request #957 from moucho/master
Updated Spanish translation
2018-08-12 14:20:53 +01:00
Dan Brown
d69ba6b47a Updated composer dependancies 2018-08-12 13:42:17 +01:00
Dan Brown
098128aafb Added test to cover new language autodetect config option 2018-08-12 13:34:14 +01:00
Dan Brown
92c9837157 Fixed incorrect type error in LDAP group sync
Should fix #951
2018-08-12 13:28:40 +01:00
Marcos
18e5f86ffa Updated Spanish translation 2018-08-12 14:14:56 +02:00
Dan Brown
c860645a5a Tweaked bug report template to request hosting method 2018-08-12 13:12:47 +01:00
Dan Brown
fcb93dc7c8 Added option to disable public lang autodetect
Also cleaned up localization middleware a little.
Closes #944
2018-08-12 13:10:55 +01:00
Dan Brown
fcdb39e428 Merge pull request #942 from marcusforsberg/master
Updated Swedish translation
2018-08-12 12:51:00 +01:00
Dan Brown
1b3e1863f4 Merge pull request #948 from houbaron/fix/Chinese_translation
Fix/Chinese translation
2018-08-12 12:37:35 +01:00
Dan Brown
fbc2175789 Merge pull request #952 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-08-12 12:36:28 +01:00
Leonardo Martinez
8099c431bb Updated 'Spanish Argentina' translation. 2018-08-06 10:46:53 -03:00
Baron Hou
efbfe0f7af Update Traditional Chinese 2018-08-05 17:07:13 +08:00
Baron Hou
66402b474c Update Simplified Chinese 2018-08-05 17:05:41 +08:00
Dan Brown
c3986cedfc Added shelve icon, improved migration, added role permission
Icon is placeholder for now
Migration will now copy permissions from Books to apply to shelves.
Role view updated with visibility on shelve permission
2018-08-04 12:45:45 +01:00
Dan Brown
b5a2d3c1c4 Merge remote-tracking branch 'origin' into bookshelves 2018-08-04 11:35:01 +01:00
Khalid
a6862362c1 added Arabic to locales 2018-08-03 19:00:51 +03:00
Khalid
7d4ec0d633 added Arabic language to drop-down list. 2018-08-03 18:58:27 +03:00
Khalid
85544c04a9 translated 2018-08-03 18:51:18 +03:00
Khalid
0a8ff6ffad translated 2018-08-03 18:34:39 +03:00
Khalid
04274078c4 translated 2018-08-02 06:38:33 +03:00
Khalid
aac9fbf236 translated 2018-08-01 18:25:44 +03:00
Khalid
08e290f3ea translated 2018-07-30 12:58:12 +03:00
Khalid
86602854ac translated 2018-07-30 12:54:32 +03:00
marcusforsberg
f47f0e05d6 Updated Swedish translation 2018-07-30 09:35:34 +03:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
c83a51f7e2 Merge pull request #904 from lommes/903-socialite-discord
add everything needed to use discord as social login provider
2018-07-29 16:18:10 +01:00
Dan Brown
b922c8029e Merge pull request #933 from nicobubulle/master
French translation update
2018-07-29 16:04:39 +01:00
Dan Brown
653761e67d Merge pull request #925 from alex2702/fix/835
Fixed German translations for notifications
2018-07-29 16:03:29 +01:00
Dan Brown
d59ff132ab Delete ISSUE_TEMPLATE.md 2018-07-29 15:55:13 +01:00
Dan Brown
e6e740b2a1 Update issue templates 2018-07-29 15:54:53 +01:00
Dan Brown
af6f4e6c8c Updated pagination to use theme colour 2018-07-29 15:44:10 +01:00
Dan Brown
69a0f8d502 Prevented error notification being visible on load
Fixes #897

Also made design a little more compact
2018-07-29 15:34:54 +01:00
Dan Brown
6d35fb5237 Updated packages via npm audit 2018-07-28 15:03:29 +01:00
Khalid
6eb63a1e03 translated 2018-07-28 14:13:12 +03:00
Khalid
8774f1a320 translated 2018-07-28 13:02:19 +03:00
Khalid
6ca8ccd330 translated 2018-07-28 12:47:35 +03:00
Khalid
df88ffa159 translated 2018-07-28 00:13:33 +03:00
Khalid
dcbb8ad960 translated 2018-07-24 21:40:49 +03:00
Khalid
0f2ffa9545 translated to Arabic 2018-07-24 21:36:12 +03:00
Khalid
6bae16f7e9 fully translated "common" and partially translated "entities" 2018-07-24 18:11:55 +03:00
kmoj86
f5ca7ab1c8 Update entities.php 2018-07-23 18:19:17 +03:00
Khalid
7dd11decb8 partial file translation 2018-07-23 13:08:07 +03:00
nicobubulle
79d0f707e6 French translation update 2018-07-22 18:20:09 +02:00
alex2702
369dc02e78 Fixed German translations for notifications 2018-07-15 21:26:55 +02:00
Dan Brown
9d2e65b73d Merge branch 'brennanmurphy-master' 2018-07-15 19:36:28 +01:00
Dan Brown
f421d83627 Added ability to set custom ldap group -> role mapping
Added input in role form to allow matching against custom names.
Changed default mapping to use role display name instead of the hidden
DB name.
2018-07-15 19:34:42 +01:00
Dan Brown
be2ca9d4bb Refactored out the LDAP repo 2018-07-15 18:21:45 +01:00
Dan Brown
17bca662a7 Added tests to cover ldap group mapping
Also updated .env.example formatting.
Updated how LdapRepo uses Ldap so can be mocked by testing.
2018-07-15 17:57:25 +01:00
Dan Brown
1776204870 Merge branch 'master' of git://github.com/brennanmurphy/BookStack into brennanmurphy-master 2018-07-14 14:17:55 +01:00
Dan Brown
985e214d94 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-07-14 14:14:37 +01:00
Dan Brown
2bcc159fd6 Allowed creating pages in visible chapters in invisible books
Fixes permissions with test to cover in the event a page is created,
with permission, in a chapter but the user does not have permission to
see the parent book.

Fixes #912
2018-07-14 14:12:29 +01:00
Dan Brown
fb7c12438d Merge pull request #918 from DeehSlash/fix/pt_br_locale
Adds missing pt_BR strings
2018-07-14 10:31:18 +01:00
Dan Brown
b2cd363539 Added browserlist, Tweaked md scrollToText ot use ES6 2018-07-14 10:20:49 +01:00
Dan Brown
f668bee88b Merge branch 'master' into feature/edit-link-headers 2018-07-14 09:36:14 +01:00
André Luiz da Silva
642f2760cc Improves and adds missing pt_BR strings 2018-07-10 15:10:21 -03:00
Brennan Murphy
37aa8b05f8 Update files to PSR-2 standards 2018-07-02 17:27:43 +00:00
Brennan Murphy
d640cc1eee LDAP groups sync to Bookstack roles.
Closes #75
2018-07-02 17:09:39 +00:00
Abijeet Patro
c2d6e98985 Merge pull request #907 from BookStackApp/fix/date-image-manager
Changes the way the date is displayed in image-manager.
2018-07-02 00:34:30 +05:30
Dan Brown
84b4fe6176 Merge pull request #886 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-07-01 16:21:38 +01:00
Dan Brown
decdf5714b Merge pull request #865 from moucho/master
New strings from 0.22 release for Spanish translation
2018-07-01 16:20:47 +01:00
Dan Brown
9da600caf9 Merge pull request #906 from BookStackApp/bug/revision-wrap
Fixes issue with code not wrapping on revision page.
2018-07-01 16:18:49 +01:00
Dan Brown
45aee2a1c1 Merge pull request #874 from BookStackApp/fix/gototext
Fixes undefined error when clicking on link under page navigation.
2018-07-01 16:13:10 +01:00
Abijeet
f5df5ac7d5 Changes the way the date is displayed in image-manager.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-30 11:04:12 +05:30
Abijeet
fb29f4119d Fixes issue with code not wrapping on revision page.
Closes #888

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-30 09:49:55 +05:30
Timo B
93795b6eda add everything needed to use discord as social login provider 2018-06-28 09:01:36 +02:00
Leonardo Martinez
f7b808a9e6 Merge remote-tracking branch 'upstream/master' 2018-06-26 09:37:20 -03:00
Dan Brown
4948b443b6 Started work on bookshelves 2018-06-24 13:38:19 +01:00
Abijeet Patro
448068e318 Merge pull request #892 from BookStackApp/fix/884
Fixes issue with having to click the delete icon for attachment twice.
2018-06-17 18:29:32 +05:30
Abijeet
7d81a95156 Fixes issue with having to click the delete icon for attachment twice.
Fixes #884

This is happening because -

Due to the limitations of modern JavaScript (and the abandonment of Object.observe), Vue cannot detect property addition or deletion. Since Vue performs the getter/setter conversion process during instance initialization, a property must be present in the data object in order for Vue to convert it and make it reactive.

Source: https://vuejs.org/v2/guide/reactivity.html

Also added padding to the icons in the attachment section.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-17 14:21:31 +05:30
Leonardo Martinez
a9bf2ed398 Updated 'Spanish Argentina' translation. 2018-06-13 10:12:36 -03:00
Abijeet
771f781e7f Fixes a corner case with exclamation in the ID.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:29:30 +05:30
Abijeet
78be8535f7 Removed previous code that is now unneeded
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:19:03 +05:30
Abijeet
6c4c1ccb58 Changed the way we were displaying the edit icon.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:06:23 +05:30
Abijeet
562225a77b Added code to set the cursor at end of line while scrolling.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 17:04:54 +05:30
Abijeet
b936e1f403 Added code to handle scroll for markdown.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-10 13:11:10 +05:30
Dan Brown
b3cc3130f0 Added copy button to codemirror-rendered code blocks
Closes #858
2018-06-09 10:41:01 +01:00
Abijeet
0363fc4ea1 Fixes undefined error when clicking on page navigation links.
Fixes #873

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-03 14:24:55 +05:30
Abijeet
134a96fa32 Adds edit icon to each header in the page.
Towards #618

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-06-03 13:47:07 +05:30
Marcos
56f444a8a7 New strings 2018-05-30 01:41:25 +02:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
86f43c8a65 Fixed incorrect tag from removing ng tags 2018-05-28 11:06:11 +01:00
Dan Brown
d886c6a32e Removed old ng tags, Fixed header spacing
Also prevent pointer error on custom home page
2018-05-28 10:33:38 +01:00
Dan Brown
f399e60910 Made header link spacing a little more even 2018-05-27 20:32:06 +01:00
Dan Brown
173eaf1c98 Made comments section more subtle
Also removed spacing from within details above active restrictions
2018-05-27 20:20:13 +01:00
Dan Brown
64eabaf882 Fixed search icon overalapping input in header
Fixes #859
2018-05-27 19:51:32 +01:00
Dan Brown
6b84a76af1 Merge branch 'drawing_updates' 2018-05-27 19:42:25 +01:00
Dan Brown
2bd6ba9895 Added maintenance view with image-cleanup 2018-05-27 19:40:07 +01:00
Dan Brown
1df0bcaf85 Made image cleanup safer
Also fixed drawing update in markdown editor.
Added shortcut for MD editor to view drawing manager.
2018-05-27 14:33:50 +01:00
Dan Brown
c31e6a03ce Added command to clean-up old images, Unfinished 2018-05-20 18:16:01 +01:00
Dan Brown
61c9324229 Removed old image versions test 2018-05-20 17:12:44 +01:00
Dan Brown
8c4c8cd95b Updated secure-images option to not effect image name
Instead only the image path is altered.
Also fixed image manger mode not changing on button press.
2018-05-20 16:47:53 +01:00
Dan Brown
0c9c1e4c6b Reverted work on revisions
Improved linkage of drawings and image manager.
Updated image updates to create new versions.
2018-05-20 16:41:14 +01:00
Dan Brown
9ec114641c Merge pull request #846 from moucho/master
Updated Spanish translation
2018-05-20 15:20:33 +01:00
Dan Brown
295c7918a4 Merge pull request #851 from vriic/master
Update german translation
2018-05-20 12:06:44 +01:00
Dan Brown
3ac34b5849 Merge pull request #802 from marcusforsberg/master
Updated Swedish translation
2018-05-20 12:05:11 +01:00
Dan Brown
6e7adcc095 Embedded SVG icons in css/js files
Allows removal of hacky /icon endpoint solution.
Fixes PDF exports with WKHTML and allows the icon to show in HTML
exports.

Fixes #796
2018-05-20 11:55:23 +01:00
Dan Brown
a1ecdcacba Fixed attachment error handling, Allowed all link types
Related to #812
2018-05-20 11:06:10 +01:00
Dan Brown
019b8196ad Merge branch 'feature/615' 2018-05-20 10:13:34 +01:00
Dan Brown
63f96c1c6f Reorganised home and robots views
Extracted home view sidebar into own view.
Moved home and robot views into 'common' folder so that we only have
layouts in the top-level views folder.
2018-05-20 10:11:56 +01:00
Dan Brown
8df9dab80a Merge branch 'master' into feature/615 2018-05-20 09:51:45 +01:00
Dan Brown
93147f4340 Prevented back-to-top showing on flexbox-body pages
Fixes #824
2018-05-20 09:48:11 +01:00
Dan Brown
77727e7e50 Update session config to match laravel
Includes option to set secure cookies via env.
Closes #817
2018-05-20 09:38:27 +01:00
Dan Brown
9f4c64a676 Codemirror mode now correct for c-like langs
Fixes #849
2018-05-20 09:32:15 +01:00
Nikolai Nikolajevic
e0ebae19aa Update: Übersetzung 2018-05-20 03:00:55 +02:00
Dan Brown
6cdb943916 Started work on revisions in image manager 2018-05-19 18:44:40 +01:00
Dan Brown
d3d8ddbe52 Improved 404 handling and fixed editor error
404 handling now not a hack-around and uses Laravel 'fallback' routes
instead. Prevents errors with the session when you have mulitple errors
on a page where a post/put/delete is made.
2018-05-19 17:01:33 +01:00
Marcos
57c312ec3f Updated Spanish translation 2018-05-18 03:10:49 +02:00
Dan Brown
13ad0031d6 Drawings now generate revisions, not replace
Updated drawing update test to accomodate.
Image deletion system now takes revisions into account.
2018-05-13 17:41:35 +01:00
Dan Brown
d5b922aa50 Started work on drawing revisions
Improved sidebar and selection styling of image manager.
Allowed image manager imageType to be changed on open.
Created models for image revisions.
2018-05-13 12:07:38 +01:00
Abijeet
28823c4fae Changed the location of the "view-toggle" to be under the books views.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 18:26:35 +05:30
Abijeet
b6bb078e0a removed some added CSS as it was causing unintended sideffects.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 17:28:10 +05:30
Abijeet
8254c3be8d Added the book view toggle option on the homepage.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 14:16:05 +05:30
Abijeet
47cb99a2d6 Added test cases.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-12 13:07:28 +05:30
Abijeet
86b2ddbd28 Implemented displaying of the books list on home page. 2018-05-10 09:05:18 +05:30
Abijeet
2e4863edb1 Added an option to set books as the default homepage.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-05-09 08:26:49 +05:30
Dan Brown
b0d027a4a9 Repaired other uses of entity-meta view 2018-04-30 15:12:10 +01:00
Dan Brown
0c3c8fc9c3 Updated npm dependancies 2018-04-30 14:54:54 +01:00
Dan Brown
624c568008 Revamped tag styling 2018-04-30 14:35:15 +01:00
Dan Brown
58a0a59d7e Cleaned details sidebar box and merged with permissions 2018-04-30 13:53:04 +01:00
Dan Brown
3d0d7f8be2 Updated version for next block of development 2018-04-30 13:52:22 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
eb5069ca66 Attempted to fix failing time-based test 2018-04-22 20:06:46 +01:00
Dan Brown
0306253c45 Merge branch 'master' of github.com:BookStackApp/BookStack 2018-04-22 12:26:21 +01:00
Dan Brown
71b6f09128 Applied phpcs findings 2018-04-22 12:25:32 +01:00
Dan Brown
67e0c3d2a5 Improved export base64 encoding of images
Now will use set storage mechanism to find image files.
Fixes #786

Added test to cover
2018-04-22 12:23:43 +01:00
Dan Brown
6aeb1387aa Fixed licence badge 2018-04-22 11:26:11 +01:00
Dan Brown
fa83e6bda4 Merge pull request #806 from leomartinez/master
Updated 'Spanish Argentina' translation.
2018-04-21 13:46:07 +01:00
Leonardo Martinez
ae89e05a25 Updated 'Spanish Argentina' translation. 2018-04-17 09:45:57 -03:00
Dan Brown
a50153d221 Slimmed down testing DB sized and improved permission caching 2018-04-14 22:17:47 +01:00
Dan Brown
cdb1c7ef88 Added destination permission checking to entity move 2018-04-14 18:47:13 +01:00
Dan Brown
0f7b0ad45a Added ability to copy a page
In 'More' menu alongside move.
Allows you to move if you have permission to create within the new
target parent.
Closes #673
2018-04-14 18:00:16 +01:00
marcusforsberg
6c5304a3de Updated Swedish translation 2018-04-14 18:09:09 +02:00
Dan Brown
d34b91f2c9 Updated move card width and made sidebar order more consistent 2018-04-14 16:23:16 +01:00
Dan Brown
dfadaa28f6 Updated reset-password flow design
Fixes #800
2018-04-14 16:16:29 +01:00
Dan Brown
fae564ff32 Merge pull request #798 from abno85/loc-de_DE
Update German localization
2018-04-14 16:02:48 +01:00
Dan Brown
502b22a0f2 Merge pull request #783 from moucho/master
Completely overhaul of the Spanish translation,  added missing strings
2018-04-14 16:01:25 +01:00
Dan Brown
f9feeef5c9 Merge pull request #780 from jasoncheng7115/master
Add Language zh_TW
2018-04-14 16:00:03 +01:00
Dan Brown
a6674a5a5e Merge pull request #767 from msaus/update_japanese_translation
update japanese translation
2018-04-14 15:58:35 +01:00
Dan Brown
fb18576259 Merge pull request #768 from BookStackApp/feature/tinymce-insert-video
Adds the media plugin to TinyMCE to allow insertion of videos.
2018-04-14 15:57:08 +01:00
Abijeet
7238a01f89 Moved the code to the wysiwyg-editor file.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:41:35 +05:30
Abijeet
93f92e9e16 Updated the TinyMCE to version 4.7.9.
Added some code to remove the box-shadow around the TinyMCE toolbar.

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:30:34 +05:30
Abijeet
d92efd4edc Adds the media plugin to TinyMCE to allow insertion of videos.
Fixes #266

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-04-14 18:30:28 +05:30
abno85
448f7d091b Add comment_add
Missed 'comment_add' the last time
2018-04-13 08:44:54 +02:00
abno85
11470b85f9 Update German localization
Added a few missing strings and (hopefully) fixed my borked first commit.
2018-04-12 13:48:07 +02:00
Marcos
9c07619099 orthography 2018-04-05 03:57:35 +02:00
Marcos
60a224f7a1 Missing comma 2018-04-05 02:58:32 +02:00
Marcos
e392e1fd8b Completely overhaul of the Spanish translation, added missing strings 2018-04-03 15:58:04 +02:00
Jason Cheng
64d5763d08 Add zh_TW Locales.
Add zh_TW Locales.
2018-04-02 16:09:23 +08:00
Jason Cheng
007059273e Add translate.
Add translate.
2018-04-02 15:54:06 +08:00
Jason Cheng
106432ee4e Added Language zh_TW
Added Language zh_TW
2018-04-02 15:03:07 +08:00
Dan Brown
0ade9b5b9b Refactored moment.js out of app
Reduces bundle size by 25%
2018-04-01 14:10:44 +01:00
Dan Brown
736d7118b0 Refactored js file structure to be standard throughout app
Still work to be done but a good start in standardisation.
2018-04-01 13:21:11 +01:00
Dan Brown
b612cf9e4c Refactored out page-display system 2018-04-01 12:46:27 +01:00
Dan Brown
1a72208d27 Added configurable robots.txt file.
Deleted old static file.
Default output depends on app-public setting.
Otherwise can be overidden in `.env` file via `ALLOW_ROBOTS`
Otherwise view file can be customized.

Fixes #779
2018-03-31 12:41:40 +01:00
Dan Brown
7f437c2e3c Fixed issue where cover images don't save on older books
Ensured an existing ID is always provided to image-picker.js.
Fixes #773
2018-03-31 11:21:22 +01:00
Dan Brown
cfdf5b93d9 Merge branch 'v0.20' to gain export fix 2018-03-30 15:45:34 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
3cd08382e9 Fixed export style paths 2018-03-30 15:31:39 +01:00
Dan Brown
58a6b2df7d Merge branch 'master' of github.com:BookStackApp/BookStack 2018-03-30 14:10:36 +01:00
Dan Brown
582158f70e Added tags to chapters and books
Closes #121
2018-03-30 14:09:51 +01:00
msaus
03ee3d21ba Merge branch 'master' into update_japanese_translation 2018-03-28 11:58:14 +09:00
Abijeet Patro
b99229a5c3 Merge pull request #769 from BookStackApp/psr-2-fixes
PSR2 fixes after running `./vendor/bin/phpcbf`
2018-03-28 01:09:09 +05:30
Abijeet
2fc513984d PSR2 fixes after running ./vendor/bin/phpcbf
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-03-28 01:07:01 +05:30
Soseki Masao
34499658d5 update japanese translation 2018-03-27 16:18:38 +09:00
Dan Brown
a8f18c0102 Updated COC with criticism point 2018-03-25 18:16:53 +01:00
Dan Brown
c85cace48b Updated readme, license and added code of conduct 2018-03-25 18:12:49 +01:00
368 changed files with 13250 additions and 6401 deletions

2
.browserslistrc Normal file
View File

@@ -0,0 +1,2 @@
>0.25%
not op_mini all

View File

@@ -56,6 +56,8 @@ TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
@@ -67,6 +69,13 @@ LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Mail settings
MAIL_DRIVER=smtp

84
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@@ -0,0 +1,84 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
### Project Maintainer Standards
Project maintainers should generally follow these additional standards:
* Avoid using a negative or harsh tone in communication, Even if the other party
is being negative themselves.
* When providing criticism, try to make it constructive to lead the other person
down the correct path.
* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)
in mind when deciding what's in scope of the Project.
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior. In addition, Project
maintainers are responsible for following the standards themselves.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

View File

@@ -1,21 +0,0 @@
### For Feature Requests
Desired Feature:
### For Bug Reports
* BookStack Version *(Found in settings, Please don't put 'latest')*:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Current Behavior
##### Steps to Reproduce

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Your Configuration (please complete the following information):**
- Exact BookStack Version (Found in settings):
- PHP Version:
- Hosting Method (Nginx/Apache/Docker):
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Describe the feature you'd like**
A clear description of the feature you'd like implemented in BookStack.
**Describe the benefits this feature would bring to BookStack users**
Explain the measurable benefits this feature would achieve.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

@@ -21,3 +21,4 @@ nbproject
.buildpath
.project
.settings/
webpack-stats.json

View File

@@ -1,6 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016 Dan Brown
Copyright (c) 2018 Dan Brown and the BookStack Project contributors
https://github.com/BookStackApp/BookStack/graphs/contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -31,6 +31,9 @@ class Attachment extends Ownable
*/
public function getUrl()
{
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
return baseUrl('/attachments/' . $this->id);
}
}

View File

@@ -48,14 +48,6 @@ class Book extends Entity
{
return $this->belongsTo(Image::class, 'image_id');
}
/*
* Get the edit url for this book.
* @return string
*/
public function getEditUrl()
{
return $this->getUrl() . '/edit';
}
/**
* Get all pages within this book.
@@ -75,6 +67,15 @@ class Book extends Entity
return $this->hasMany(Chapter::class);
}
/**
* Get the shelves this book is contained within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function shelves()
{
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length

83
app/Bookshelf.php Normal file
View File

@@ -0,0 +1,83 @@
<?php namespace BookStack;
class Bookshelf extends Entity
{
protected $table = 'bookshelves';
public $searchFactor = 3;
protected $fillable = ['name', 'description', 'image_id'];
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
}
/**
* Get the url for this bookshelf.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
if ($path !== false) {
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return baseUrl('/shelves/' . urlencode($this->slug));
}
/**
* Returns BookShelf cover image, if cover does not exists return default cover image.
* @param int $width - Width of the image
* @param int $height - Height of the image
* @return string
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
if (!$this->image_id) {
return $default;
}
try {
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
} catch (\Exception $err) {
$cover = $default;
}
return $cover;
}
/**
* Get the cover image of the book
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
{
return $this->belongsTo(Image::class, 'image_id');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -6,8 +6,6 @@ class Chapter extends Entity
protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $with = ['book'];
/**
* Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo

View File

@@ -0,0 +1,85 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\ImageService;
use Illuminate\Console\Command;
use Symfony\Component\Console\Output\OutputInterface;
class CleanupImages extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:cleanup-images
{--a|all : Include images that are used in page revisions}
{--f|force : Actually run the deletions}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Cleanup images and drawings';
protected $imageService;
/**
* Create a new command instance.
* @param ImageService $imageService
*/
public function __construct(ImageService $imageService)
{
$this->imageService = $imageService;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$checkRevisions = $this->option('all') ? false : true;
$dryRun = $this->option('force') ? false : true;
if (!$dryRun) {
$proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
if (!$proceed) {
return;
}
}
$deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($deleted);
if ($dryRun) {
$this->comment('Dry run, No images have been deleted');
$this->comment($deleteCount . ' images found that would have been deleted');
$this->showDeletedImages($deleted);
$this->comment('Run with -f or --force to perform deletions');
return;
}
$this->showDeletedImages($deleted);
$this->comment($deleteCount . ' images deleted');
}
protected function showDeletedImages($paths)
{
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
return;
}
if (count($paths) > 0) {
$this->line('Images to delete:');
}
foreach ($paths as $path) {
$this->line($path);
}
}
}

View File

@@ -152,7 +152,7 @@ class Entity extends Ownable
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter'];
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
@@ -168,10 +168,10 @@ class Entity extends Ownable
*/
public function getShortName($length = 25)
{
if (strlen($this->name) <= $length) {
if (mb_strlen($this->name) <= $length) {
return $this->name;
}
return substr($this->name, 0, $length - 3) . '...';
return mb_substr($this->name, 0, $length - 3) . '...';
}
/**
@@ -197,9 +197,8 @@ class Entity extends Ownable
* @param $path
* @return string
*/
public function getUrl($path)
public function getUrl($path = '/')
{
return '/';
return $path;
}
}

View File

@@ -4,8 +4,6 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -33,6 +31,7 @@ class Handler extends ExceptionHandler
*
* @param \Exception $e
* @return mixed
* @throws Exception
*/
public function report(Exception $e)
{
@@ -65,30 +64,12 @@ class Handler extends ExceptionHandler
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
return $this->loadErrorMiddleware($request, function ($request) use ($e) {
$message = $e->getMessage() ?: trans('errors.404_page_not_found');
return response()->view('errors/404', ['message' => $message], 404);
});
return \Route::respondWithRoute('fallback');
}
return parent::render($request, $e);
}
/**
* Load the middleware required to show state/session-enabled error pages.
* @param Request $request
* @param $callback
* @return mixed
*/
protected function loadErrorMiddleware(Request $request, $callback)
{
$middleware = (\Route::getMiddlewareGroups()['web_errors']);
return (new Pipeline($this->container))
->send($request)
->through($middleware)
->then($callback);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e

View File

@@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
class SocialSignInAccountNotUsed extends SocialSignInException
{
}

View File

@@ -103,7 +103,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'url|min:1|max:255'
'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
@@ -131,7 +131,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|url|min:1|max:255'
'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
@@ -184,6 +184,7 @@ class AttachmentController extends Controller
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
* @throws NotFoundException
*/
public function get($attachmentId)
{
@@ -200,10 +201,7 @@ class AttachmentController extends Controller
}
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return response($attachmentContents, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
]);
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
}
/**

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\LdapService;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
@@ -36,18 +37,21 @@ class LoginController extends Controller
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $ldapService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param LdapService $ldapService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->ldapService = $ldapService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
@@ -66,6 +70,7 @@ class LoginController extends Controller
* @param Authenticatable $user
* @return \Illuminate\Http\RedirectResponse
* @throws AuthException
* @throws \BookStack\Exceptions\LdapException
*/
protected function authenticated(Request $request, Authenticatable $user)
{
@@ -96,6 +101,11 @@ class LoginController extends Controller
auth()->login($user);
}
// Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user, $request->get($this->username()));
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
@@ -125,6 +135,7 @@ class LoginController extends Controller
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function getSocialLogin($socialDriver)
{

View File

@@ -2,7 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
@@ -16,6 +16,7 @@ use Illuminate\Http\Response;
use Validator;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
use Laravel\Socialite\Contracts\User as SocialUser;
class RegisterController extends Controller
{
@@ -133,25 +134,28 @@ class RegisterController extends Controller
* The registrations flow for all users.
* @param array $userData
* @param bool|false|SocialAccount $socialAccount
* @param bool $emailVerified
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
*/
protected function registerUser(array $userData, $socialAccount = false)
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
{
if (setting('registration-restrict')) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
$registrationRestrict = setting('registration-restrict');
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
}
$newUser = $this->userRepo->registerNew($userData);
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
if ($socialAccount) {
$newUser->socialAccounts()->save($socialAccount);
}
if (setting('registration-confirmation') || setting('registration-restrict')) {
if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
$newUser->save();
try {
@@ -250,7 +254,6 @@ class RegisterController extends Controller
* @throws SocialSignInException
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws ConfirmationEmailException
*/
public function socialCallback($socialDriver, Request $request)
{
@@ -267,12 +270,24 @@ class RegisterController extends Controller
}
$action = session()->pull('social-callback');
// Attempt login or fall-back to register if allowed.
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
if ($action == 'login') {
return $this->socialAuthService->handleLoginCallback($socialDriver);
try {
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
} catch (SocialSignInAccountNotUsed $exception) {
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
throw $exception;
}
}
if ($action == 'register') {
return $this->socialRegisterCallback($socialDriver);
return $this->socialRegisterCallback($socialDriver, $socialUser);
}
return redirect()->back();
}
@@ -288,15 +303,16 @@ class RegisterController extends Controller
/**
* Register a new user after a registration callback.
* @param $socialDriver
* @param string $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws UserRegistrationException
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
protected function socialRegisterCallback($socialDriver)
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance
$userData = [
@@ -304,6 +320,6 @@ class RegisterController extends Controller
'email' => $socialUser->getEmail(),
'password' => str_random(30)
];
return $this->registerUser($userData, $socialAccount);
return $this->registerUser($userData, $socialAccount, $emailVerified);
}
}

View File

@@ -299,10 +299,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$pdfContent = $this->exportService->bookToPdf($book);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
}
/**
@@ -314,10 +311,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
]);
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
}
/**
@@ -328,10 +322,7 @@ class BookController extends Controller
public function exportPlainText($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToPlainText($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
]);
$textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
}

View File

@@ -0,0 +1,243 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
class BookshelfController extends Controller
{
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Display a listing of the book.
* @return Response
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
]);
}
/**
* Show the form for creating a new bookshelf.
* @return Response
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @return Response
*/
public function store(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
return redirect($bookshelf->getUrl());
}
/**
* Display the specified bookshelf.
* @param String $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
]);
}
/**
* Show the form for editing the specified bookshelf.
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
}
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, string $slug)
{
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
/**
* Shows the page to confirm deletion
* @param $slug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
}
/**
* Remove the specified bookshelf from storage.
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function destroy(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function restrict(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
}
/**
* Copy the permissions of a bookshelf to the child books.
* @param string $slug
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
}
}

View File

@@ -107,17 +107,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
if ($chapter->name !== $request->get('name')) {
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
}
$chapter->fill($request->all());
$chapter->updated_by = user()->id;
$chapter->save();
$this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return redirect($chapter->getUrl());
}
@@ -253,10 +250,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
}
/**
@@ -269,10 +263,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
}
/**
@@ -284,10 +275,7 @@ class ChapterController extends Controller
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
$chapterText = $this->exportService->chapterToPlainText($chapter);
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
}
}

View File

@@ -136,7 +136,6 @@ abstract class Controller extends BaseController
/**
* Create the response for when a request fails validation.
*
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
@@ -151,4 +150,18 @@ abstract class Controller extends BaseController
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
/**
* Create a response that forces a download in the browser.
* @param string $content
* @param string $fileName
* @return \Illuminate\Http\Response
*/
protected function downloadResponse(string $content, string $fileName)
{
return response()->make($content, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
]);
}
}

View File

@@ -33,23 +33,42 @@ class HomeController extends Controller
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
// Custom homepage
$customHomepage = false;
$homepageSetting = setting('app-homepage');
if ($homepageSetting) {
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
$homepageOption = setting('app-homepage-type', 'default');
if (!in_array($homepageOption, $homepageOptions)) {
$homepageOption = 'default';
}
$view = $customHomepage ? 'home-custom' : 'home';
return view($view, [
$commonData = [
'activity' => $activity,
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'customHomepage' => $customHomepage
]);
];
if ($homepageOption === 'bookshelves') {
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
return view('common.home-book', $data);
}
if ($homepageOption === 'page') {
$homepageSetting = setting('app-homepage', '0:');
$id = intval(explode(':', $homepageSetting)[0]);
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
$this->entityRepo->renderPage($customHomepage, true);
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
}
return view('common.home', $commonData);
}
/**
@@ -89,27 +108,6 @@ class HomeController extends Controller
]);
}
/**
* Get an icon via image request.
* Can provide a 'color' parameter with hex value to color the icon.
* @param $iconName
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getIcon($iconName, Request $request)
{
$attrs = [];
if ($request->filled('color')) {
$attrs['fill'] = '#' . $request->get('color');
}
$icon = icon($iconName, $attrs);
return response($icon, 200, [
'Content-Type' => 'image/svg+xml',
'Cache-Control' => 'max-age=3600',
]);
}
/**
* Get custom head HTML, Used in ajax calls to show in editor.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
@@ -118,4 +116,28 @@ class HomeController extends Controller
{
return view('partials/custom-head-content');
}
/**
* Show the view for /robots.txt
* @return $this
*/
public function getRobots()
{
$sitePublic = setting('app-public', false);
$allowRobots = config('app.allow_robots');
if ($allowRobots === null) {
$allowRobots = $sitePublic;
}
return response()
->view('common/robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
/**
* Show the route for 404 responses.
*/
public function getNotFound()
{
return response()->view('errors/404', [], 404);
}
}

View File

@@ -164,32 +164,6 @@ class ImageController extends Controller
return response()->json($image);
}
/**
* Replace the data content of a drawing.
* @param string $id
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function replaceDrawing(string $id, Request $request)
{
$this->validate($request, [
'image' => 'required|string'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-update', $image);
try {
$image = $this->imageRepo->replaceDrawingContent($image, $imageBase64Data);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
@@ -245,26 +219,29 @@ class ImageController extends Controller
}
/**
* Deletes an image and all thumbnail/image files
* Show the usage of an image on pages.
* @param EntityRepo $entityRepo
* @param Request $request
* @param int $id
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function destroy(EntityRepo $entityRepo, Request $request, $id)
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
// Check if this image is used on any pages
$isForced = in_array($request->get('force', ''), [true, 'true']);
if (!$isForced) {
$pageSearch = $entityRepo->searchForImage($image->url);
if ($pageSearch !== false) {
return response()->json($pageSearch, 400);
}
}
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}

View File

@@ -5,7 +5,6 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -38,11 +37,18 @@ class PageController extends Controller
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
* @throws NotFoundException
*/
public function create($bookSlug, $chapterSlug = null)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -52,7 +58,7 @@ class PageController extends Controller
return redirect($draft->getUrl());
}
// Otherwise show edit view
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
@@ -71,8 +77,14 @@ class PageController extends Controller
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@@ -93,7 +105,7 @@ class PageController extends Controller
public function editDraft($bookSlug, $pageId)
{
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->checkOwnablePermission('page-create', $draft->parent);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
@@ -119,12 +131,10 @@ class PageController extends Controller
]);
$input = $request->all();
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$book = $draftPage->book;
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$parent = $draftPage->parent;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {
@@ -275,11 +285,10 @@ class PageController extends Controller
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
$updateTime = $draft->updated_at->timestamp;
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $utcUpdateTimestamp
'timestamp' => $updateTime
]);
}
@@ -445,6 +454,40 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Deletes a revision using the id of the specified revision.
* @param string $bookSlug
* @param string $pageSlug
* @param int $revId
* @throws NotFoundException
* @throws BadRequestException
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroyRevision($bookSlug, $pageSlug, $revId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {
throw new NotFoundException("Revision #{$revId} not found");
}
// Get the current revision for the page
$currentRevision = $page->getCurrentRevision();
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
@@ -457,10 +500,7 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
]);
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
/**
@@ -474,10 +514,7 @@ class PageController extends Controller
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$page->html = $this->entityRepo->renderPage($page);
$containedHtml = $this->exportService->pageToContainedHtml($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
]);
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
}
/**
@@ -489,11 +526,8 @@ class PageController extends Controller
public function exportPlainText($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$containedHtml = $this->exportService->pageToPlainText($page);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
]);
$pageText = $this->exportService->pageToPlainText($page);
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
@@ -586,6 +620,8 @@ class PageController extends Controller
return redirect()->back();
}
$this->checkOwnablePermission('page-create', $parent);
$this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
@@ -593,12 +629,70 @@ class PageController extends Controller
return redirect($page->getUrl());
}
/**
* Show the view to copy a page.
* @param string $bookSlug
* @param string $pageSlug
* @return mixed
* @throws NotFoundException
*/
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
'book' => $page->book,
'page' => $page
]);
}
/**
* Create a copy of a page within the requested target destination.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return mixed
* @throws NotFoundException
*/
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
$parent = $page->chapter ? $page->chapter : $page->book;
} else {
$stringExploded = explode(':', $entitySelection);
$entityType = $stringExploded[0];
$entityId = intval($stringExploded[1]);
try {
$parent = $this->entityRepo->getById($entityType, $entityId);
} catch (\Exception $e) {
session()->flash(trans('entities.selected_book_chapter_not_found'));
return redirect()->back();
}
}
$this->checkOwnablePermission('page-create', $parent);
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
session()->flash('success', trans('entities.pages_copy_success'));
return redirect($pageCopy->getUrl());
}
/**
* Set the permissions for this page.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{

View File

@@ -78,6 +78,7 @@ class PermissionController extends Controller
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
*/
public function updateRole($id, Request $request)
{

View File

@@ -89,16 +89,17 @@ class SearchController extends Controller
{
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm)['results'];
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames);
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Services\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Setting;
@@ -13,7 +14,7 @@ class SettingController extends Controller
public function index()
{
$this->checkPermission('settings-manage');
$this->setPageTitle('Settings');
$this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
@@ -43,4 +44,48 @@ class SettingController extends Controller
session()->flash('success', trans('settings.settings_save_success'));
return redirect('/settings');
}
/**
* Show the page for application maintenance.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showMaintenance()
{
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/maintenance', ['version' => $version]);
}
/**
* Action to clean-up images in the system.
* @param Request $request
* @param ImageService $imageService
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function cleanupImages(Request $request, ImageService $imageService)
{
$this->checkPermission('settings-manage');
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
$dryRun = !($request->has('confirm'));
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
}

View File

@@ -252,7 +252,7 @@ class UserController extends Controller
return $this->currentUser->id == $id;
});
$viewType = $request->get('book_view_type');
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
@@ -262,4 +262,27 @@ class UserController extends Controller
return redirect()->back(302, [], "/settings/users/$id");
}
/**
* Update the user's preferred shelf-list display setting.
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function switchShelfView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
}
}

View File

@@ -33,14 +33,6 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
'web_errors' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',
'bindings',

View File

@@ -2,9 +2,13 @@
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
class Localization
{
protected $rtlLocales = ['ar'];
/**
* Handle an incoming request.
*
@@ -15,21 +19,38 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
if (user()->isDefault()) {
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) {
continue;
}
$locale = $lang;
break;
}
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
// Set text direction
if (in_array($locale, $this->rtlLocales)) {
config()->set('app.rtl', true);
}
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
/**
* Autodetect the visitors locale by matching locales in their headers
* against the locales supported by BookStack.
* @param Request $request
* @param string $default
* @return string
*/
protected function autoDetectLocale(Request $request, string $default)
{
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (in_array($lang, $availableLocales)) {
return $lang;
}
}
return $default;
}
}

View File

@@ -9,10 +9,11 @@ class Image extends Ownable
/**
* Get a thumbnail for this image.
* @param int $width
* @param int $height
* @param int $width
* @param int $height
* @param bool|false $keepRatio
* @return string
* @throws \Exception
*/
public function getThumb($width, $height, $keepRatio = false)
{

View File

@@ -6,7 +6,6 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
public $textField = 'text';
/**
@@ -29,6 +28,15 @@ class Page extends Entity
return $this->belongsTo(Book::class);
}
/**
* Get the parent item
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function parent()
{
return $this->chapter_id ? $this->chapter() : $this->book();
}
/**
* Get the chapter that this page is in, If applicable.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
@@ -104,4 +112,13 @@ class Page extends Entity
$htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
/**
* Get the current revision for the page if existing
* @return \BookStack\PageRevision|null
*/
public function getCurrentRevision()
{
return $this->revisions()->first();
}
}

View File

@@ -3,6 +3,7 @@
namespace BookStack\Providers;
use BookStack\Activity;
use BookStack\Image;
use BookStack\Services\ImageService;
use BookStack\Services\PermissionService;
use BookStack\Services\ViewService;
@@ -57,6 +58,7 @@ class CustomFacadeProvider extends ServiceProvider
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class)

View File

@@ -20,6 +20,7 @@ class EventServiceProvider extends ServiceProvider
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
],
];

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Repos;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Exceptions\NotFoundException;
@@ -18,6 +19,10 @@ use Illuminate\Support\Collection;
class EntityRepo
{
/**
* @var Bookshelf
*/
public $bookshelf;
/**
* @var Book $book
@@ -67,6 +72,7 @@ class EntityRepo
/**
* EntityRepo constructor.
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
@@ -77,6 +83,7 @@ class EntityRepo
* @param SearchService $searchService
*/
public function __construct(
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page,
@@ -86,11 +93,13 @@ class EntityRepo
TagRepo $tagRepo,
SearchService $searchService
) {
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->pageRevision = $pageRevision;
$this->entities = [
'bookshelf' => $this->bookshelf,
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
@@ -331,6 +340,17 @@ class EntityRepo
->skip($count * $page)->take($count)->get();
}
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
{
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
@@ -492,14 +512,19 @@ class EntityRepo
public function createFromInput($type, $input = [], $book = false)
{
$isChapter = strtolower($type) === 'chapter';
$entity = $this->getEntity($type)->newInstance($input);
$entity->slug = $this->findSuitableSlug($type, $entity->name, false, $isChapter ? $book->id : false);
$entity->created_by = user()->id;
$entity->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entity) : $entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
$this->searchService->indexEntity($entity);
return $entity;
$entityModel = $this->getEntity($type)->newInstance($input);
$entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
$entityModel->created_by = user()->id;
$entityModel->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
/**
@@ -518,11 +543,38 @@ class EntityRepo
$entityModel->fill($input);
$entityModel->updated_by = user()->id;
$entityModel->save();
if (isset($input['tags'])) {
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
}
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
/**
* Sync the books assigned to a shelf from a comma-separated list
* of book IDs.
* @param Bookshelf $shelf
* @param string $books
*/
public function updateShelfBooks(Bookshelf $shelf, string $books)
{
$ids = explode(',', $books);
// Check books exist and match ordering
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
$syncData = [];
foreach ($ids as $index => $id) {
if ($bookIds->contains($id)) {
$syncData[$id] = ['order' => $index];
}
}
$shelf->books()->sync($syncData);
}
/**
* Change the book that an entity belongs to.
* @param string $type
@@ -583,6 +635,30 @@ class EntityRepo
return $slug;
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
@@ -611,6 +687,43 @@ class EntityRepo
return $draftPage;
}
/**
* Create a copy of a page in a new location with a new name.
* @param Page $page
* @param Entity $newParent
* @param string $newName
* @return Page
*/
public function copyPage(Page $page, Entity $newParent, $newName = '')
{
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
$newChapter = $newParent->isA('chapter') ? $newParent : null;
$copyPage = $this->getDraftPage($newBook, $newChapter);
$pageData = $page->getAttributes();
// Update name
if (!empty($newName)) {
$pageData['name'] = $newName;
}
// Copy tags from previous page if set
if ($page->tags) {
$pageData['tags'] = [];
foreach ($page->tags as $tag) {
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
}
}
// Set priority
if ($newParent->isA('chapter')) {
$pageData['priority'] = $this->getNewChapterPriority($newParent);
} else {
$pageData['priority'] = $this->getNewBookPriority($newParent);
}
return $this->publishPageDraft($copyPage, $pageData);
}
/**
* Saves a page revision into the system.
* @param Page $page
@@ -633,10 +746,13 @@ class EntityRepo
$revision->revision_number = $page->revision_count;
$revision->save();
// Clear old revisions
if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) {
$this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(50)->take(5)->delete();
$revisionLimit = config('app.revision_limit');
if ($revisionLimit !== false) {
$revisionsToDelete = $this->pageRevision->where('page_id', '=', $page->id)
->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
if ($revisionsToDelete->count() > 0) {
$this->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
}
}
return $revision;
@@ -774,7 +890,9 @@ class EntityRepo
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) return $html;
if (count($matches) === 0) {
return $html;
}
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
@@ -793,30 +911,6 @@ class EntityRepo
return strip_tags($html);
}
/**
* Get a new draft page instance.
* @param Book $book
* @param Chapter|bool $chapter
* @return Page
*/
public function getDraftPage(Book $book, $chapter = false)
{
$page = $this->page->newInstance();
$page->name = trans('entities.pages_initial_name');
$page->created_by = user()->id;
$page->updated_by = user()->id;
$page->draft = true;
if ($chapter) {
$page->chapter_id = $chapter->id;
}
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
/**
* Search for image usage within page content.
* @param $imageString
@@ -1105,9 +1199,22 @@ class EntityRepo
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
* Destroy a bookshelf instance
* @param Bookshelf $shelf
* @throws \Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
$this->destroyEntityCommonRelations($shelf);
$shelf->delete();
}
/**
* Destroy the provided book and all its child entities.
* @param Book $book
* @throws NotifyException
* @throws \Throwable
*/
public function destroyBook(Book $book)
{
@@ -1117,17 +1224,14 @@ class EntityRepo
foreach ($book->chapters as $chapter) {
$this->destroyChapter($chapter);
}
\Activity::removeEntity($book);
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$this->searchService->deleteEntityTerms($book);
$this->destroyEntityCommonRelations($book);
$book->delete();
}
/**
* Destroy a chapter and its relations.
* @param Chapter $chapter
* @throws \Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@@ -1137,11 +1241,7 @@ class EntityRepo
$page->save();
}
}
\Activity::removeEntity($chapter);
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$this->searchService->deleteEntityTerms($chapter);
$this->destroyEntityCommonRelations($chapter);
$chapter->delete();
}
@@ -1149,23 +1249,18 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws \Throwable
*/
public function destroyPage(Page $page)
{
\Activity::removeEntity($page);
$page->views()->delete();
$page->tags()->delete();
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Check if set as custom homepage
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
$this->destroyEntityCommonRelations($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
@@ -1174,4 +1269,48 @@ class EntityRepo
$page->delete();
}
/**
* Destroy or handle the common relations connected to an entity.
* @param Entity $entity
* @throws \Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
\Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$this->permissionService->deleteJointPermissionsForEntity($entity);
$this->searchService->deleteEntityTerms($entity);
}
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
$shelfBooks = $bookshelf->books()->get();
$updatedBookCount = 0;
foreach ($shelfBooks as $book) {
if (!userCan('restrictions-manage', $book)) {
continue;
}
$book->permissions()->delete();
$book->restricted = $bookshelf->restricted;
$book->permissions()->createMany($shelfPermissions);
$book->save();
$this->permissionService->buildJointPermissionsForEntity($book);
$updatedBookCount++;
}
return $updatedBookCount;
}
}

View File

@@ -153,17 +153,6 @@ class ImageRepo
return $image;
}
/**
* Replace the image content of a drawing.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function replaceDrawingContent(Image $image, string $base64Uri)
{
return $this->imageService->replaceImageDataFromBase64Uri($image, $base64Uri);
}
/**
* Update the details of an image via an array of properties.
@@ -183,13 +172,14 @@ class ImageRepo
/**
* Destroys an Image object along with its files and thumbnails.
* Destroys an Image object along with its revisions, files and thumbnails.
* @param Image $image
* @return bool
* @throws \Exception
*/
public function destroyImage(Image $image)
{
$this->imageService->destroyImage($image);
$this->imageService->destroy($image);
return true;
}
@@ -200,7 +190,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
private function loadThumbs(Image $image)
protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
@@ -250,7 +240,7 @@ class ImageRepo
*/
public function isValidType($type)
{
$validTypes = ['drawing', 'gallery', 'cover', 'system', 'user'];
$validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
}
}

View File

@@ -80,7 +80,7 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always has all permissions.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
@@ -90,13 +90,18 @@ class PermissionsRepo
$role = $this->role->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
$permissions = array_merge($permissions, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
}
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);

View File

@@ -76,14 +76,15 @@ class UserRepo
return $query->paginate($count);
}
/**
/**
* Creates a new user and attaches a role to them.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function registerNew(array $data)
public function registerNew(array $data, $verifyEmail = false)
{
$user = $this->create($data);
$user = $this->create($data, $verifyEmail);
$this->attachDefaultRole($user);
// Get avatar from gravatar and save
@@ -94,15 +95,14 @@ class UserRepo
/**
* Give a user the default role. Used when creating a new user.
* @param $user
* @param User $user
*/
public function attachDefaultRole($user)
public function attachDefaultRole(User $user)
{
$roleId = setting('registration-role');
if ($roleId === false) {
$roleId = $this->role->first()->id;
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
$user->attachRoleId($roleId);
}
$user->attachRoleId($roleId);
}
/**
@@ -141,15 +141,17 @@ class UserRepo
/**
* Create a new basic instance of user.
* @param array $data
* @param boolean $verifyEmail
* @return User
*/
public function create(array $data)
public function create(array $data, $verifyEmail = false)
{
return $this->user->forceCreate([
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => false
'email_confirmed' => $verifyEmail
]);
}
@@ -166,7 +168,7 @@ class UserRepo
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroyImage($image);
Images::destroy($image);
}
}

View File

@@ -3,7 +3,7 @@
class Role extends Model
{
protected $fillable = ['display_name', 'description'];
protected $fillable = ['display_name', 'description', 'external_auth_id'];
/**
* The roles that belong to the role.

View File

@@ -9,14 +9,16 @@ class ExportService
{
protected $entityRepo;
protected $imageService;
/**
* ExportService constructor.
* @param $entityRepo
*/
public function __construct(EntityRepo $entityRepo)
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
{
$this->entityRepo = $entityRepo;
$this->imageService = $imageService;
}
/**
@@ -24,6 +26,7 @@ class ExportService
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
* @throws \Throwable
*/
public function pageToContainedHtml(Page $page)
{
@@ -38,6 +41,7 @@ class ExportService
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
* @throws \Throwable
*/
public function chapterToContainedHtml(Chapter $chapter)
{
@@ -56,6 +60,7 @@ class ExportService
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
* @throws \Throwable
*/
public function bookToContainedHtml(Book $book)
{
@@ -71,6 +76,7 @@ class ExportService
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
* @throws \Throwable
*/
public function pageToPdf(Page $page)
{
@@ -85,6 +91,7 @@ class ExportService
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
* @throws \Throwable
*/
public function chapterToPdf(Chapter $chapter)
{
@@ -103,6 +110,7 @@ class ExportService
* Convert a book to a PDF file
* @param Book $book
* @return string
* @throws \Throwable
*/
public function bookToPdf(Book $book)
{
@@ -118,6 +126,7 @@ class ExportService
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
* @throws \Exception
*/
protected function htmlToPdf($html)
{
@@ -146,45 +155,14 @@ class ExportService
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgString = $imgMatch;
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$isLocal = strpos(trim($srcString), 'http') !== 0;
if ($isLocal) {
$pathString = public_path(trim($srcString, '/'));
} else {
$pathString = $srcString;
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
}
// Attempt to find local files even if url not absolute
$base = baseUrl('/');
if (strpos($srcString, $base) === 0) {
$isLocal = true;
$relString = str_replace($base, '', $srcString);
$pathString = public_path(trim($relString, '/'));
}
if ($isLocal && !file_exists($pathString)) {
continue;
}
try {
if ($isLocal) {
$imageContent = file_get_contents($pathString);
} else {
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageContent = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
}
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {
$newImageString = '';
}
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
}
}

View File

@@ -3,11 +3,11 @@
use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use DB;
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;
use Illuminate\Contracts\Cache\Repository as Cache;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -17,15 +17,18 @@ class ImageService extends UploadService
protected $imageTool;
protected $cache;
protected $storageUrl;
protected $image;
/**
* ImageService constructor.
* @param $imageTool
* @param $fileSystem
* @param $cache
* @param Image $image
* @param ImageManager $imageTool
* @param FileSystem $fileSystem
* @param Cache $cache
*/
public function __construct(ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
{
$this->image = $image;
$this->imageTool = $imageTool;
$this->cache = $cache;
parent::__construct($fileSystem);
@@ -82,31 +85,6 @@ class ImageService extends UploadService
return $this->saveNew($name, $data, $type, $uploadedTo);
}
/**
* Replace the data for an image via a Base64 encoded string.
* @param Image $image
* @param string $base64Uri
* @return Image
* @throws ImageUploadException
*/
public function replaceImageDataFromBase64Uri(Image $image, string $base64Uri)
{
$splitData = explode(';base64,', $base64Uri);
if (count($splitData) < 2) {
throw new ImageUploadException("Invalid base64 image data provided");
}
$data = base64_decode($splitData[1]);
$storage = $this->getStorage();
try {
$storage->put($image->path, $data);
} catch (Exception $e) {
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $image->path]));
}
return $image;
}
/**
* Gets an image from url and saves it to the database.
* @param $url
@@ -140,16 +118,16 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
if ($secureUploads) {
$imageName = str_random(16) . '-' . $imageName;
}
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
}
$fullPath = $imagePath . $imageName;
if ($secureUploads) {
$fullPath = $imagePath . str_random(16) . '-' . $imageName;
}
try {
$storage->put($fullPath, $imageData);
@@ -172,28 +150,20 @@ class ImageService extends UploadService
$imageDetails['updated_by'] = $userId;
}
$image = (new Image());
$image = $this->image->newInstance();
$image->forceFill($imageDetails)->save();
return $image;
}
/**
* Get the storage path, Dependant of storage type.
* @param Image $image
* @return mixed|string
*/
protected function getPath(Image $image)
{
return $image->path;
}
/**
* Checks if the image is a gif. Returns true if it is, else false.
* @param Image $image
* @return boolean
*/
protected function isGif(Image $image) {
return strtolower(pathinfo($this->getPath($image), PATHINFO_EXTENSION)) === 'gif';
protected function isGif(Image $image)
{
return strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'gif';
}
/**
@@ -211,11 +181,11 @@ class ImageService extends UploadService
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
if ($keepRatio && $this->isGif($image)) {
return $this->getPublicUrl($this->getPath($image));
return $this->getPublicUrl($image->path);
}
$thumbDirName = '/' . ($keepRatio ? 'scaled-' : 'thumbs-') . $width . '-' . $height . '/';
$imagePath = $this->getPath($image);
$imagePath = $image->path;
$thumbFilePath = dirname($imagePath) . $thumbDirName . basename($imagePath);
if ($this->cache->has('images-' . $image->id . '-' . $thumbFilePath) && $this->cache->get('images-' . $thumbFilePath)) {
@@ -261,43 +231,51 @@ class ImageService extends UploadService
*/
public function getImageData(Image $image)
{
$imagePath = $this->getPath($image);
$imagePath = $image->path;
$storage = $this->getStorage();
return $storage->get($imagePath);
}
/**
* Destroys an Image object along with its files and thumbnails.
* Destroy an image along with its revisions, thumbnails and remaining folders.
* @param Image $image
* @return bool
* @throws Exception
*/
public function destroyImage(Image $image)
public function destroy(Image $image)
{
$this->destroyImagesFromPath($image->path);
$image->delete();
}
/**
* Destroys an image at the given path.
* Searches for image thumbnails in addition to main provided path..
* @param string $path
* @return bool
*/
protected function destroyImagesFromPath(string $path)
{
$storage = $this->getStorage();
$imageFolder = dirname($this->getPath($image));
$imageFileName = basename($this->getPath($image));
$imageFolder = dirname($path);
$imageFileName = basename($path);
$allImages = collect($storage->allFiles($imageFolder));
// Delete image files
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
$expectedIndex = strlen($imagePath) - strlen($imageFileName);
return strpos($imagePath, $imageFileName) === $expectedIndex;
});
$storage->delete($imagesToDelete->all());
// Cleanup of empty folders
foreach ($storage->directories($imageFolder) as $directory) {
$foldersInvolved = array_merge([$imageFolder], $storage->directories($imageFolder));
foreach ($foldersInvolved as $directory) {
if ($this->isFolderEmpty($directory)) {
$storage->deleteDirectory($directory);
}
}
if ($this->isFolderEmpty($imageFolder)) {
$storage->deleteDirectory($imageFolder);
}
$image->delete();
return true;
}
@@ -320,6 +298,93 @@ class ImageService extends UploadService
return $image;
}
/**
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
* Checks based off of only the image name.
* Could be much improved to be more specific but kept it generic for now to be safe.
*
* Returns the path of the images that would be/have been deleted.
* @param bool $checkRevisions
* @param bool $dryRun
* @param array $types
* @return array
*/
public function deleteUnusedImages($checkRevisions = true, $dryRun = true, $types = ['gallery', 'drawio'])
{
$types = array_intersect($types, ['gallery', 'drawio']);
$deletedPaths = [];
$this->image->newQuery()->whereIn('type', $types)
->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
foreach ($images as $image) {
$searchQuery = '%' . basename($image->path) . '%';
$inPage = DB::table('pages')
->where('html', 'like', $searchQuery)->count() > 0;
$inRevision = false;
if ($checkRevisions) {
$inRevision = DB::table('page_revisions')
->where('html', 'like', $searchQuery)->count() > 0;
}
if (!$inPage && !$inRevision) {
$deletedPaths[] = $image->path;
if (!$dryRun) {
$this->destroy($image);
}
}
}
});
return $deletedPaths;
}
/**
* Convert a image URI to a Base64 encoded string.
* Attempts to find locally via set storage method first.
* @param string $uri
* @return null|string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function imageUriToBase64(string $uri)
{
$isLocal = strpos(trim($uri), 'http') !== 0;
// Attempt to find local files even if url not absolute
$base = baseUrl('/');
if (!$isLocal && strpos($uri, $base) === 0) {
$isLocal = true;
$uri = str_replace($base, '', $uri);
}
$imageData = null;
if ($isLocal) {
$uri = trim($uri, '/');
$storage = $this->getStorage();
if ($storage->exists($uri)) {
$imageData = $storage->get($uri);
}
} else {
try {
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
$imageData = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
throw new \Exception("Image fetch failed, Received error: " . $err);
}
} catch (\Exception $e) {
}
}
if ($imageData === null) {
return null;
}
return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
}
/**
* Gets a public facing url for an image by checking relevant environment variables.
* @param string $filePath

View File

@@ -1,7 +1,11 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use BookStack\Repos\UserRepo;
use BookStack\Role;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
/**
* Class LdapService
@@ -14,15 +18,55 @@ class LdapService
protected $ldap;
protected $ldapConnection;
protected $config;
protected $userRepo;
protected $enabled;
/**
* LdapService constructor.
* @param Ldap $ldap
* @param UserRepo $userRepo
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserRepo $userRepo)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
$this->userRepo = $userRepo;
$this->enabled = config('auth.method') === 'ldap';
}
/**
* Check if groups should be synced.
* @return bool
*/
public function shouldSyncGroups()
{
return $this->enabled && $this->config['user_to_groups'] !== false;
}
/**
* Search for attributes for a specific user on the ldap
* @param string $userName
* @param array $attributes
* @return null|array
* @throws LdapException
*/
private function getUserWithAttributes($userName, $attributes)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
if ($users['count'] === 0) {
return null;
}
return $users[0];
}
/**
@@ -34,21 +78,13 @@ class LdapService
*/
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'];
$emailAttr = $this->config['email_attribute'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) {
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
$user = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
@@ -162,4 +198,172 @@ class LdapService
}
return strtr($filterString, $newAttrs);
}
/**
* Get the groups a user is a part of on ldap
* @param string $userName
* @return array
* @throws LdapException
*/
public function getUserGroups($userName)
{
$groupsAttr = $this->config['group_attribute'];
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
if ($user === null) {
return [];
}
$userGroups = $this->groupFilter($user);
$userGroups = $this->getGroupsRecursive($userGroups, []);
return $userGroups;
}
/**
* Get the parent groups of an array of groups
* @param array $groupsArray
* @param array $checked
* @return array
* @throws LdapException
*/
private function getGroupsRecursive($groupsArray, $checked)
{
$groups_to_add = [];
foreach ($groupsArray as $groupName) {
if (in_array($groupName, $checked)) {
continue;
}
$groupsToAdd = $this->getGroupGroups($groupName);
$groups_to_add = array_merge($groups_to_add, $groupsToAdd);
$checked[] = $groupName;
}
$groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
if (!empty($groups_to_add)) {
return $this->getGroupsRecursive($groupsArray, $checked);
} else {
return $groupsArray;
}
}
/**
* Get the parent groups of a single group
* @param string $groupName
* @return array
* @throws LdapException
*/
private function getGroupGroups($groupName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$baseDn = $this->config['base_dn'];
$groupsAttr = strtolower($this->config['group_attribute']);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
if ($groups['count'] === 0) {
return [];
}
$groupGroups = $this->groupFilter($groups[0]);
return $groupGroups;
}
/**
* Filter out LDAP CN and DN language in a ldap search return
* Gets the base CN (common name) of the string
* @param string $ldapSearchReturn
* @return array
*/
protected function groupFilter($ldapSearchReturn)
{
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = [];
$count = 0;
if (isset($ldapSearchReturn[$groupsAttr]['count'])) {
$count = (int) $ldapSearchReturn[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
$dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0];
}
}
return $ldapGroups;
}
/**
* Sync the LDAP groups to the user roles for the current user
* @param \BookStack\User $user
* @param string $username
* @throws LdapException
*/
public function syncGroups(User $user, string $username)
{
$userLdapGroups = $this->getUserGroups($username);
// Get the ids for the roles from the names
$ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups);
// Sync groups
if ($this->config['remove_from_groups']) {
$user->roles()->sync($ldapGroupsAsRoles);
$this->userRepo->attachDefaultRole($user);
} else {
$user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
}
}
/**
* Match an array of group names from LDAP to BookStack system roles.
* Formats LDAP group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchLdapGroupsToSystemsRoles(array $groupNames)
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});
return $matchedRoles->pluck('id');
}
/**
* Check a role against an array of group names to see if it matches.
* Checked against role 'external_auth_id' if set otherwise the name of the role.
* @param Role $role
* @param array $groupNames
* @return bool
*/
protected function roleMatchesGroupNames(Role $role, array $groupNames)
{
if ($role->external_auth_id) {
$externalAuthIds = explode(',', strtolower($role->external_auth_id));
foreach ($externalAuthIds as $externalAuthId) {
if (in_array(trim($externalAuthId), $groupNames)) {
return true;
}
}
return false;
}
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
return in_array($roleName, $groupNames);
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\EntityPermission;
@@ -25,6 +26,7 @@ class PermissionService
public $book;
public $chapter;
public $page;
public $bookshelf;
protected $db;
@@ -38,22 +40,31 @@ class PermissionService
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Role $role
*/
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{
public function __construct(
JointPermission $jointPermission,
EntityPermission $entityPermission,
Role $role,
Connection $db,
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page
) {
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
// TODO - Update so admin still goes through filters
}
/**
@@ -67,13 +78,19 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param Entity[] $entities
*/
protected function readyEntityCache()
protected function readyEntityCache($entities = [])
{
$this->entityCache = [
'books' => collect(),
'chapters' => collect()
];
$this->entityCache = [];
foreach ($entities as $entity) {
$type = $entity->getType();
if (!isset($this->entityCache[$type])) {
$this->entityCache[$type] = collect();
}
$this->entityCache[$type]->put($entity->id, $entity);
}
}
/**
@@ -83,17 +100,14 @@ class PermissionService
*/
protected function getBook($bookId)
{
if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
return $this->entityCache['books']->get($bookId);
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
return $this->entityCache['book']->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;
}
@@ -105,17 +119,14 @@ class PermissionService
*/
protected function getChapter($chapterId)
{
if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
return $this->entityCache['chapters']->get($chapterId);
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
return $this->entityCache['chapter']->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;
}
@@ -159,6 +170,12 @@ class PermissionService
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@@ -174,11 +191,26 @@ class PermissionService
}]);
}
/**
* @param Collection $shelves
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
{
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all());
}
$this->createManyJointPermissions($shelves, $roles);
}
/**
* Build joint permissions for an array of books
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
@@ -203,6 +235,7 @@ class PermissionService
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
@@ -213,7 +246,9 @@ class PermissionService
return;
}
$entities[] = $entity->book;
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity->isA('page') && $entity->chapter_id) {
$entities[] = $entity->chapter;
@@ -225,13 +260,13 @@ class PermissionService
}
}
$this->deleteManyJointPermissionsForEntities($entities);
$this->buildJointPermissionsForEntities(collect($entities));
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
* @throws \Throwable
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
@@ -250,9 +285,15 @@ class PermissionService
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
@@ -279,6 +320,7 @@ class PermissionService
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
@@ -288,6 +330,7 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
* @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
@@ -314,10 +357,11 @@ class PermissionService
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param array $roles
* @throws \Throwable
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
@@ -342,7 +386,7 @@ class PermissionService
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->getRelationValue('permissions') as $permission) {
foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
@@ -408,7 +452,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
if ($entity->isA('book')) {
if ($entity->isA('book') || $entity->isA('bookshelf')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
@@ -480,11 +524,6 @@ class PermissionService
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
{
if ($this->isAdmin()) {
$this->clean();
return true;
}
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
@@ -577,17 +616,16 @@ class PermissionService
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
if (!$this->isAdmin()) {
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
// Add joint permission filter
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function ($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
@@ -615,11 +653,6 @@ class PermissionService
});
}
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
}
@@ -630,16 +663,13 @@ class PermissionService
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
@@ -670,11 +700,6 @@ class PermissionService
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
@@ -699,19 +724,6 @@ class PermissionService
return $q;
}
/**
* Check if the current user is an admin.
* @return bool
*/
private function isAdmin()
{
if ($this->isAdminUser === null) {
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
}
return $this->isAdminUser;
}
/**
* Get the current user
* @return User

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Bookshelf;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
@@ -13,11 +14,16 @@ use Illuminate\Support\Collection;
class SearchService
{
protected $searchTerm;
protected $bookshelf;
protected $book;
protected $chapter;
protected $page;
protected $db;
protected $permissionService;
/**
* @var Entity[]
*/
protected $entities;
/**
@@ -29,20 +35,23 @@ class SearchService
/**
* SearchService constructor.
* @param SearchTerm $searchTerm
* @param Bookshelf $bookshelf
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Connection $db
* @param PermissionService $permissionService
*/
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
public function __construct(SearchTerm $searchTerm, Bookshelf $bookshelf, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
{
$this->searchTerm = $searchTerm;
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->db = $db;
$this->entities = [
'bookshelf' => $this->bookshelf,
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
@@ -65,9 +74,10 @@ class SearchService
* @param string $entityType
* @param int $page
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @param string $action
* @return array[int, Collection];
*/
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
@@ -87,8 +97,8 @@ class SearchService
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, true);
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
if ($entityTotal > $page * $count) {
$hasMore = true;
}
@@ -147,12 +157,13 @@ class SearchService
* @param string $entityType
* @param int $page
* @param int $count
* @param string $action
* @param bool $getCount Return the total count of the search
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
if ($getCount) {
return $query->count();
}
@@ -165,9 +176,10 @@ class SearchService
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @param string $action
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page')
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
@@ -212,7 +224,7 @@ class SearchService
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
@@ -368,20 +380,12 @@ class SearchService
{
$this->searchTerm->truncate();
// Chunk through all books
$this->book->chunk(1000, function ($books) {
$this->indexEntities($books);
});
// Chunk through all chapters
$this->chapter->chunk(1000, function ($chapters) {
$this->indexEntities($chapters);
});
// Chunk through all pages
$this->page->chunk(1000, function ($pages) {
$this->indexEntities($pages);
});
foreach ($this->entities as $entityModel) {
$selectFields = ['id', 'name', $entityModel->textField];
$entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
$this->indexEntities($entities);
});
}
}
/**

View File

@@ -1,13 +1,12 @@
<?php namespace BookStack\Services;
use BookStack\Http\Requests\Request;
use GuzzleHttp\Exception\ClientException;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use Laravel\Socialite\Contracts\Factory as Socialite;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Repos\UserRepo;
use BookStack\SocialAccount;
use Laravel\Socialite\Contracts\User as SocialUser;
class SocialAuthService
{
@@ -16,7 +15,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* SocialAuthService constructor.
@@ -58,18 +57,13 @@ class SocialAuthService
/**
* Handle the social registration process on callback.
* @param $socialDriver
* @return \Laravel\Socialite\Contracts\User
* @throws SocialDriverNotConfigured
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialUser
* @throws UserRegistrationException
*/
public function handleRegistrationCallback($socialDriver)
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
@@ -84,17 +78,26 @@ class SocialAuthService
}
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* Get the social user details via the social driver.
* @param string $socialDriver
* @return SocialUser
* @throws SocialDriverNotConfigured
* @throws SocialSignInException
*/
public function handleLoginCallback($socialDriver)
public function getSocialUser(string $socialDriver)
{
$driver = $this->validateDriver($socialDriver);
// Get user details from social driver
$socialUser = $this->socialite->driver($driver)->user();
return $this->socialite->driver($driver)->user();
}
/**
* Handle the login process on a oAuth callback.
* @param $socialDriver
* @param SocialUser $socialUser
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws SocialSignInAccountNotUsed
*/
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
{
$socialId = $socialUser->getId();
// Get any attached social accounts or users
@@ -136,7 +139,7 @@ class SocialAuthService
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
}
throw new SocialSignInException($message, '/login');
throw new SocialSignInAccountNotUsed($message, '/login');
}
/**
@@ -199,8 +202,28 @@ class SocialAuthService
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
* Check if the current config for the given driver allows auto-registration.
* @param string $driver
* @return bool
*/
public function driverAutoRegisterEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_register') === true;
}
/**
* Check if the current config for the given driver allow email address auto-confirmation.
* @param string $driver
* @return bool
*/
public function driverAutoConfirmEmailEnabled(string $driver)
{
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
}
/**
* @param string $socialDriver
* @param SocialUser $socialUser
* @return SocialAccount
*/
public function fillSocialAccount($socialDriver, $socialUser)

View File

@@ -51,11 +51,13 @@ class ViewService
* @param int $count
* @param int $page
* @param bool|false|array $filterModel
* @param string $action - used for permission checking
* @return
*/
public function getPopular($count = 10, $page = 0, $filterModel = false)
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');

View File

@@ -6,9 +6,10 @@
"type": "project",
"require": {
"php": ">=7.0.0",
"laravel/framework": "5.5.*",
"fideloper/proxy": "~3.3",
"ext-tidy": "*",
"ext-dom": "*",
"laravel/framework": "~5.5.42",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
"laravel/socialite": "^3.0",
"league/flysystem-aws-s3-v3": "^1.0",
@@ -20,7 +21,9 @@
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
"socialiteproviders/gitlab": "^3.0",
"socialiteproviders/twitch": "^3.0"
"socialiteproviders/twitch": "^3.0",
"socialiteproviders/discord": "^2.0",
"doctrine/dbal": "^2.5"
},
"require-dev": {
"filp/whoops": "~2.0",

824
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,35 @@ return [
'env' => env('APP_ENV', 'production'),
/**
* Set the default view type for various lists. Can be overridden by user preferences.
* This will be used for public viewers and users that have not set a preference.
*/
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list')
],
/**
* The number of revisions to keep in the database.
* Once this limit is reached older revisions will be deleted.
* If set to false then a limit will not be enforced.
*/
'revision_limit' => env('REVISION_LIMIT', 50),
/**
* Allow <script> tags to entered within page content.
* <script> tags are escaped by default.
* Even when overridden the WYSIWYG editor may still escape script content.
*/
'allow_content_scripts' => env('ALLOW_CONTENT_SCRIPTS', false),
/**
* Override the default behaviour for allowing crawlers to crawl the instance.
* May be ignored if view has be overridden or modified.
* Defaults to null since, if not set, 'app-public' status used instead.
*/
'allow_robots' => env('ALLOW_ROBOTS', null),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
@@ -61,7 +84,32 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN'],
'locales' => ['en', 'ar', 'de', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'ja', 'pl', 'it', 'ru', 'zh_CN', 'zh_TW'],
/*
|--------------------------------------------------------------------------
| Right-to-left text control
|--------------------------------------------------------------------------
|
| Right-to-left text control is set to false by default since English
| is the primary supported application but this may be dynamically
| altered by the applications localization system.
|
*/
'rtl' => false,
/*
|--------------------------------------------------------------------------
| Auto-detect the locale for public users
|--------------------------------------------------------------------------
|
| For public users their locale can be guessed by headers sent by their
| browser. This is usually set by users in their browser settings.
| If not found the default app locale will be used.
|
*/
'auto_detect_locale' => env('APP_AUTO_LANG_PUBLIC', true),
/*
|--------------------------------------------------------------------------
@@ -229,7 +277,7 @@ return [
'Activity' => BookStack\Services\Facades\Activity::class,
'Setting' => BookStack\Services\Facades\Setting::class,
'Views' => BookStack\Services\Facades\Views::class,
'Images' => \BookStack\Services\Facades\Images::class,
'Images' => BookStack\Services\Facades\Images::class,
],

View File

@@ -48,6 +48,8 @@ return [
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
],
'google' => [
@@ -55,6 +57,8 @@ return [
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
],
'slack' => [
@@ -62,6 +66,8 @@ return [
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
'auto_register' => env('SLACK_AUTO_REGISTER', false),
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
],
'facebook' => [
@@ -69,6 +75,8 @@ return [
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
],
'twitter' => [
@@ -76,6 +84,8 @@ return [
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
],
'azure' => [
@@ -84,6 +94,8 @@ return [
'tenant' => env('AZURE_TENANT', false),
'redirect' => env('APP_URL') . '/login/service/azure/callback',
'name' => 'Microsoft Azure',
'auto_register' => env('AZURE_AUTO_REGISTER', false),
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
],
'okta' => [
@@ -92,6 +104,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/okta/callback',
'base_url' => env('OKTA_BASE_URL'),
'name' => 'Okta',
'auto_register' => env('OKTA_AUTO_REGISTER', false),
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
],
'gitlab' => [
@@ -100,6 +114,8 @@ return [
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
'name' => 'GitLab',
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
],
'twitch' => [
@@ -107,6 +123,17 @@ return [
'client_secret' => env('TWITCH_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
'name' => 'Twitch',
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
],
'discord' => [
'client_id' => env('DISCORD_APP_ID'),
'client_secret' => env('DISCORD_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/discord/callback',
'name' => 'Discord',
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
],
'ldap' => [
@@ -118,6 +145,9 @@ return [
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
]
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
]
];

View File

@@ -109,7 +109,7 @@ return [
|
*/
'cookie' => 'laravel_session',
'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
/*
|--------------------------------------------------------------------------
@@ -135,7 +135,7 @@ return [
|
*/
'domain' => null,
'domain' => env('SESSION_DOMAIN', null),
/*
|--------------------------------------------------------------------------
@@ -148,6 +148,34 @@ return [
|
*/
'secure' => false,
'secure' => env('SESSION_SECURE_COOKIE', false),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. You are free to modify this option if needed.
|
*/
'http_only' => true,
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| do not enable this as other CSRF protection services are in place.
|
| Supported: "lax", "strict"
|
*/
'same_site' => null,
];

View File

@@ -21,6 +21,14 @@ $factory->define(BookStack\User::class, function ($faker) {
];
});
$factory->define(BookStack\Bookshelf::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => str_random(10),
'description' => $faker->paragraph
];
});
$factory->define(BookStack\Book::class, function ($faker) {
return [
'name' => $faker->sentence,

View File

@@ -12,13 +12,7 @@ class CreateBooksTable extends Migration
*/
public function up()
{
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('books', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
Schema::create('books', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('slug')->indexed();

View File

@@ -12,13 +12,9 @@ class CreatePagesTable extends Migration
*/
public function up()
{
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('pages', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->integer('book_id');
$table->integer('chapter_id');

View File

@@ -12,12 +12,7 @@ class CreateChaptersTable extends Migration
*/
public function up()
{
$pdo = \DB::connection()->getPdo();
$mysqlVersion = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
$requiresISAM = strpos($mysqlVersion, '5.5') === 0;
Schema::create('chapters', function (Blueprint $table) use ($requiresISAM) {
if($requiresISAM) $table->engine = 'MyISAM';
Schema::create('chapters', function (Blueprint $table) {
$table->increments('id');
$table->integer('book_id');
$table->string('slug')->indexed();

View File

@@ -12,10 +12,13 @@ class AddSearchIndexes extends Migration
*/
public function up()
{
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
// This was removed for v0.24 since these indexes are removed anyway
// and will cause issues for db engines that don't support such indexes.
// $prefix = DB::getTablePrefix();
// DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
// DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
// DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
}
/**
@@ -25,14 +28,28 @@ class AddSearchIndexes extends Migration
*/
public function down()
{
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
});
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
});
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
});
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$pages = $sm->listTableDetails('pages');
$books = $sm->listTableDetails('books');
$chapters = $sm->listTableDetails('chapters');
if ($pages->hasIndex('search')) {
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
});
}
if ($books->hasIndex('search')) {
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
});
}
if ($chapters->hasIndex('search')) {
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
});
}
}
}

View File

@@ -12,10 +12,13 @@ class FulltextWeighting extends Migration
*/
public function up()
{
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
// This was removed for v0.24 since these indexes are removed anyway
// and will cause issues for db engines that don't support such indexes.
// $prefix = DB::getTablePrefix();
// DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
}
/**
@@ -25,14 +28,27 @@ class FulltextWeighting extends Migration
*/
public function down()
{
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('name_search');
});
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('name_search');
});
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('name_search');
});
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$pages = $sm->listTableDetails('pages');
$books = $sm->listTableDetails('books');
$chapters = $sm->listTableDetails('chapters');
if ($pages->hasIndex('name_search')) {
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('name_search');
});
}
if ($books->hasIndex('name_search')) {
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('name_search');
});
}
if ($chapters->hasIndex('name_search')) {
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('name_search');
});
}
}
}

View File

@@ -74,10 +74,6 @@ class CreateJointPermissionsTable extends Migration
// Update admin role with system name
DB::table('roles')->where('name', '=', 'admin')->update(['system_name' => 'admin']);
// Generate the new entity jointPermissions
$restrictionService = app(\BookStack\Services\PermissionService::class);
$restrictionService->buildJointPermissions();
}
/**

View File

@@ -26,21 +26,31 @@ class CreateSearchIndexTable extends Migration
$table->index('score');
});
// Drop search indexes
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
$sm = Schema::getConnection()->getDoctrineSchemaManager();
$pages = $sm->listTableDetails('pages');
$books = $sm->listTableDetails('books');
$chapters = $sm->listTableDetails('chapters');
app(\BookStack\Services\SearchService::class)->indexAllEntities();
if ($pages->hasIndex('search')) {
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
}
if ($books->hasIndex('search')) {
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
}
if ($chapters->hasIndex('search')) {
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
}
}
/**
@@ -50,14 +60,17 @@ class CreateSearchIndexTable extends Migration
*/
public function down()
{
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
// This was removed for v0.24 since these indexes are removed anyway
// and will cause issues for db engines that don't support such indexes.
// $prefix = DB::getTablePrefix();
// DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
// DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
// DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
// DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
// DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
Schema::dropIfExists('search_terms');
}
}

View File

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

View File

@@ -0,0 +1,101 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateBookshelvesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('bookshelves', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 200);
$table->string('slug', 200);
$table->text('description');
$table->integer('created_by')->nullable()->default(null);
$table->integer('updated_by')->nullable()->default(null);
$table->boolean('restricted')->default(false);
$table->integer('image_id')->nullable()->default(null);
$table->timestamps();
$table->index('slug');
$table->index('created_by');
$table->index('updated_by');
$table->index('restricted');
});
Schema::create('bookshelves_books', function (Blueprint $table) {
$table->integer('bookshelf_id')->unsigned();
$table->integer('book_id')->unsigned();
$table->integer('order')->unsigned();
$table->foreign('bookshelf_id')->references('id')->on('bookshelves')
->onUpdate('cascade')->onDelete('cascade');
$table->foreign('book_id')->references('id')->on('books')
->onUpdate('cascade')->onDelete('cascade');
$table->primary(['bookshelf_id', 'book_id']);
});
// Copy existing role permissions from Books
$ops = ['View All', 'View Own', 'Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own'];
foreach ($ops as $op) {
$dbOpName = strtolower(str_replace(' ', '-', $op));
$roleIdsWithBookPermission = DB::table('role_permissions')
->leftJoin('permission_role', 'role_permissions.id', '=', 'permission_role.permission_id')
->leftJoin('roles', 'roles.id', '=', 'permission_role.role_id')
->where('role_permissions.name', '=', 'book-' . $dbOpName)->get(['roles.id'])->pluck('id');
$permId = DB::table('role_permissions')->insertGetId([
'name' => 'bookshelf-' . $dbOpName,
'display_name' => $op . ' ' . 'BookShelves',
'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
]);
$rowsToInsert = $roleIdsWithBookPermission->map(function($roleId) use ($permId) {
return [
'role_id' => $roleId,
'permission_id' => $permId
];
})->toArray();
// Assign view permission to all current roles
DB::table('permission_role')->insert($rowsToInsert);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// Drop created permissions
$ops = ['bookshelf-create-all','bookshelf-create-own','bookshelf-delete-all','bookshelf-delete-own','bookshelf-update-all','bookshelf-update-own','bookshelf-view-all','bookshelf-view-own'];
$permissionIds = DB::table('role_permissions')->whereIn('name', $ops)
->get(['id'])->pluck('id')->toArray();
DB::table('permission_role')->whereIn('permission_id', $permissionIds)->delete();
DB::table('role_permissions')->whereIn('id', $permissionIds)->delete();
// Drop shelves table
Schema::dropIfExists('bookshelves_books');
Schema::dropIfExists('bookshelves');
// Drop related polymorphic items
DB::table('activities')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('views')->where('viewable_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('entity_permissions')->where('restrictable_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('tags')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('search_terms')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
DB::table('comments')->where('entity_type', '=', 'BookStack\Bookshelf')->delete();
}
}

View File

@@ -21,23 +21,29 @@ class DummyContentSeeder extends Seeder
$role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role);
factory(\BookStack\Book::class, 20)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($book) use ($editorUser) {
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($chapter) use ($editorUser, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
$byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
factory(\BookStack\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
$chapters = factory(\BookStack\Chapter::class, 3)->create($byData)
->each(function($chapter) use ($editorUser, $book, $byData){
$pages = factory(\BookStack\Page::class, 3)->make(array_merge($byData, ['book_id' => $book->id]));
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 3)->make($byData);
$book->chapters()->saveMany($chapters);
$book->pages()->saveMany($pages);
});
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook = factory(\BookStack\Book::class)->create(array_merge($byData, ['name' => 'Large book' . str_random(10)]));
$pages = factory(\BookStack\Page::class, 200)->make($byData);
$chapters = factory(\BookStack\Chapter::class, 50)->make($byData);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
$shelves = factory(\BookStack\Bookshelf::class, 10)->create($byData);
$largeBook->shelves()->attach($shelves->pluck('id'));
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Seeder;
class LargeContentSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// Create an editor user
$editorUser = factory(\BookStack\User::class)->create();
$editorRole = \BookStack\Role::getRole('editor');
$editorUser->attachRole($editorRole);
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
}

6400
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"scripts": {
"build": "webpack",
"production": "NODE_ENV=production webpack && rm -f ./public/dist/*styles.js",
"build-profile": "NODE_ENV=production webpack --profile --json > webpack-stats.json && rm -f ./public/dist/*styles.js",
"dev": "npm-run-all --parallel watch livereload",
"watch": "webpack --watch",
"livereload": "livereload ./public/dist/",
@@ -10,20 +11,20 @@
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.40",
"@babel/polyfill": "^7.0.0-beta.40",
"@babel/preset-env": "^7.0.0-beta.40",
"autoprefixer": "^8.1.0",
"babel-loader": "^8.0.0-beta.0",
"babel-polyfill": "^6.26.0",
"css-loader": "^0.28.10",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"livereload": "^0.7.0",
"node-sass": "^4.7.2",
"node-sass": "^4.9.2",
"npm-run-all": "^4.1.2",
"postcss-loader": "^2.1.1",
"sass-loader": "^6.0.7",
"style-loader": "^0.20.3",
"sass-loader": "^7.0.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.3",
"webpack": "^4.1.1",
"webpack": "^4.16.3",
"webpack-cli": "^2.0.11"
},
"dependencies": {
@@ -32,9 +33,9 @@
"codemirror": "^5.26.0",
"dropzone": "^5.4.0",
"jquery": "^3.3.1",
"jquery-sortable": "^0.9.13",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"moment": "^2.21.0",
"vue": "^2.2.6",
"vuedraggable": "^2.14.1"
},

View File

@@ -23,6 +23,7 @@
<env name="APP_ENV" value="testing"/>
<env name="APP_DEBUG" value="false"/>
<env name="APP_LANG" value="en"/>
<env name="APP_AUTO_LANG_PUBLIC" value="true"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
@@ -34,8 +35,12 @@
<env name="STORAGE_TYPE" value="local"/>
<env name="GITHUB_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GITHUB_AUTO_REGISTER" value=""/>
<env name="GITHUB_AUTO_CONFIRM_EMAIL" value=""/>
<env name="GOOGLE_APP_ID" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_APP_SECRET" value="aaaaaaaaaaaaaa"/>
<env name="GOOGLE_AUTO_REGISTER" value=""/>
<env name="GOOGLE_AUTO_CONFIRM_EMAIL" value=""/>
<env name="APP_URL" value="http://bookstack.dev"/>
</php>
</phpunit>

2
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -107,6 +107,11 @@ a, .link {
position: relative;
display: inline-block; }
.blended-links a {
color: inherit; }
.blended-links a svg {
fill: currentColor; }
/*
* Other HTML Text Elements
*/
@@ -247,6 +252,13 @@ p.secondary, p .secondary, span.secondary, .text-secondary {
color: #e27b41;
fill: #e27b41; }
.text-bookshelf {
color: #af5a5a;
fill: #af5a5a; }
.text-bookshelf:hover {
color: #af5a5a;
fill: #af5a5a; }
.text-book {
color: #009688;
fill: #009688; }
@@ -300,6 +312,7 @@ ul, ol {
ul {
padding-left: 20.8px;
padding-right: 20.8px;
list-style: disc; }
ul ul {
list-style: circle;
@@ -310,7 +323,8 @@ ul {
ol {
list-style: decimal;
padding-left: 32px; }
padding-left: 32px;
padding-right: 32px; }
li.checkbox-item, li.task-list-item {
list-style: none;
@@ -421,6 +435,8 @@ body.flexbox {
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh; }
.flex.sidebar section {
margin: 16px; }
.flex.sidebar + .flex.content {
flex: 3;
@@ -537,13 +553,40 @@ div[class^="col-"] img {
flex-direction: column;
border: 1px solid #ddd;
min-width: 100px; }
.grid-card h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px; }
.grid-card h2 a {
display: block;
width: 100%;
line-height: 1.2;
text-decoration: none; }
.grid-card p {
font-size: .85em;
margin: 0;
line-height: 1.6em; }
.grid-card .grid-card-content {
flex: 1; }
flex: 1;
border-top: 0;
border-bottom-width: 2px; }
.grid-card .grid-card-content, .grid-card .grid-card-footer {
padding: 24px; }
.grid-card .grid-card-content + .grid-card-footer {
padding-top: 0; }
.book-grid-item .grid-card-content h2 a {
color: #009688;
fill: #009688; }
.bookshelf-grid-item .grid-card-content h2 a {
color: #af5a5a;
fill: #af5a5a; }
.book-grid-item .grid-card-footer p.small {
font-size: .8em;
margin: 0; }
@media screen and (max-width: 800px) {
.grid.third {
grid-template-columns: 1fr 1fr; } }
@@ -1177,7 +1220,7 @@ div[class^="col-"] img {
display: block;
position: relative; }
.callout:before {
background-image: url("/icon/info-filled.svg?color=015380");
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+");
background-repeat: no-repeat;
content: '';
width: 1.2em;
@@ -1194,13 +1237,13 @@ div[class^="col-"] img {
background-color: #e7f3e7;
color: #376c39; }
.callout.success:before {
background-image: url("/icon/check-circle.svg?color=376c39"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+"); }
.callout.danger {
border-left-color: #E84F4F;
background-color: #fce8e8;
color: #b91818; }
.callout.danger:before {
background-image: url("/icon/danger.svg?color=b91818"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg=="); }
.callout.info {
border-left-color: #0288D1;
background-color: #d3efff;
@@ -1210,7 +1253,7 @@ div[class^="col-"] img {
background-color: #faeae0;
color: #b6531c; }
.callout.warning:before {
background-image: url("/icon/warning.svg?color=b6531c"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4="); }
.card {
margin: 16px;
@@ -1233,6 +1276,9 @@ div[class^="col-"] img {
word-wrap: break-word;
word-break: break-word; }
.sidebar .card h3, .sidebar .card .body, .sidebar .card .empty-text {
padding: 12px 16px; }
.card.drag-card {
border: 1px solid #DDD;
border-radius: 4px;
@@ -1250,6 +1296,7 @@ div[class^="col-"] img {
text-align: center;
justify-content: center;
width: 28px;
flex-grow: 0;
padding-left: 6px;
padding-right: 6px; }
.card.drag-card .handle:hover, .card.drag-card .drag-card-action:hover {
@@ -1257,7 +1304,8 @@ div[class^="col-"] img {
.card.drag-card .handle .svg-icon, .card.drag-card .drag-card-action .svg-icon {
margin-right: 0px; }
.card.drag-card > div .outline input {
margin: 12px 0; }
margin: 12px 0;
width: 100%; }
.card.drag-card > div.padded {
padding: 12px 0 !important; }
.card.drag-card .handle {
@@ -1268,13 +1316,38 @@ div[class^="col-"] img {
bottom: 0; }
.card.drag-card > div {
padding: 0 12px;
max-width: 80%; }
max-width: 80%;
flex: 1; }
.well {
background-color: #F8F8F8;
padding: 16px;
border: 1px solid #DDD; }
.tag-item {
display: inline-flex;
margin-bottom: 6px;
margin-right: 6px;
border-radius: 4px;
border: 1px solid #CCC;
overflow: hidden;
font-size: 0.85em; }
.tag-item a, .tag-item a:hover, .tag-item a:active {
padding: 4px 8px;
color: #777;
transition: background-color ease-in-out 80ms;
text-decoration: none; }
.tag-item a:hover {
background-color: rgba(255, 255, 255, 0.7); }
.tag-item svg {
fill: #888; }
.tag-item .tag-value {
border-left: 1px solid #DDD;
background-color: rgba(255, 255, 255, 0.5); }
.tag-list div:last-child .tag-item {
margin-bottom: 0; }
.input-base, .fake-input, input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
background-color: #FFF;
border-radius: 3px;
@@ -1289,9 +1362,9 @@ div[class^="col-"] img {
border: 1px solid #E84F4F; }
.input-base.pos, .pos.fake-input, input.pos[type="text"], input.pos[type="number"], input.pos[type="email"], input.pos[type="date"], input.pos[type="search"], input.pos[type="url"], input.pos[type="password"], select.pos, textarea.pos, .input-base.valid, .valid.fake-input, input.valid[type="text"], input.valid[type="number"], input.valid[type="email"], input.valid[type="date"], input.valid[type="search"], input.valid[type="url"], input.valid[type="password"], select.valid, textarea.valid {
border: 1px solid #52A256; }
.input-base.disabled, .disabled.fake-input, input.disabled[type="text"], input.disabled[type="number"], input.disabled[type="email"], input.disabled[type="date"], input.disabled[type="search"], input.disabled[type="url"], input.disabled[type="password"], select.disabled, textarea.disabled, .input-base[disabled], [disabled].fake-input, input[disabled][type="text"], input[disabled][type="number"], input[disabled][type="email"], input[disabled][type="date"], input[disabled][type="search"], input[disabled][type="url"], input[disabled][type="password"], select[disabled], textarea[disabled] {
.input-base.disabled, .disabled.fake-input, input.disabled[type="text"], input.disabled[type="number"], input.disabled[type="email"], input.disabled[type="date"], input.disabled[type="search"], input.disabled[type="url"], input.disabled[type="password"], select.disabled, textarea.disabled, .input-base[disabled], .fake-input[disabled], input[disabled][type="text"], input[disabled][type="number"], input[disabled][type="email"], input[disabled][type="date"], input[disabled][type="search"], input[disabled][type="url"], input[disabled][type="password"], select[disabled], textarea[disabled] {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); }
.input-base:focus, .fake-input:focus, input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="date"]:focus, input[type="search"]:focus, input[type="url"]:focus, input[type="password"]:focus, select:focus, textarea:focus {
.input-base:focus, .fake-input:focus, input:focus[type="text"], input:focus[type="number"], input:focus[type="email"], input:focus[type="date"], input:focus[type="search"], input:focus[type="url"], input:focus[type="password"], select:focus, textarea:focus {
outline: 0; }
.fake-input {
@@ -1472,6 +1545,8 @@ input:checked + .toggle-switch {
.form-group[collapsible].open .collapse-title label:before {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.form-group[collapsible] + .form-group[collapsible] {
margin-top: -13px; }
.inline-input-style, .title-input input[type="text"], .description-input textarea {
display: block;
@@ -1574,6 +1649,8 @@ table.table {
text-align: right; }
table.table .text-center {
text-align: center; }
table.table td.actions {
overflow: visible; }
table.no-style td {
border: 0;
@@ -1602,20 +1679,21 @@ header {
header .links {
display: inline-block;
vertical-align: top;
margin-right: 32px; }
@media screen and (max-width: 992px) {
header .links {
margin-right: 16px; } }
margin-left: 16px; }
header .links a {
display: inline-block;
padding: 16px 24px;
padding: 16px;
color: #FFF;
fill: #FFF; }
header .links a:last-child {
padding-right: 0; }
@media screen and (max-width: 992px) {
header .links a {
padding: 16px 12px; } }
header .dropdown-container {
padding-left: 16px;
padding-right: 0; }
@media screen and (max-width: 992px) {
header .links a {
padding-left: 12px;
padding-right: 12px; }
header .dropdown-container {
padding-left: 12px; } }
header .avatar, header .user-name {
display: inline-block; }
header .avatar {
@@ -1661,10 +1739,13 @@ header .search-box {
header .search-box input {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #EEE; }
header .search-box button {
color: #EEE;
fill: #EEE; }
z-index: 2; }
header .search-box button {
fill: #EEE;
z-index: 1; }
header .search-box button svg {
margin-right: 0; }
header .search-box ::-webkit-input-placeholder {
/* Chrome/Opera/Safari */
color: #DDD; }
@@ -1932,7 +2013,7 @@ header .search-box {
.book-tree .sidebar-page-list .sub-menu {
display: none;
padding-left: 0; }
.book-tree .sidebar-page-list .sub-menu.open {
.book-tree .sidebar-page-list [chapter-toggle].open + .sub-menu {
display: block; }
.sortable-page-list, .sortable-page-list ul {
@@ -1999,8 +2080,6 @@ ul.pagination {
padding: 3px 12px;
border: 1px solid #CCC;
margin-left: -1px;
color: #888;
fill: #888;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -2008,13 +2087,7 @@ ul.pagination {
ul.pagination a.disabled, ul.pagination span.disabled {
cursor: not-allowed; }
ul.pagination li.active span {
background-color: rgba(2, 136, 209, 0.8);
color: #EEE;
fill: #EEE;
border-color: rgba(2, 136, 209, 0.8); }
ul.pagination a {
color: #0288D1;
fill: #0288D1; }
color: #FFF; }
.compact ul.pagination {
margin: 0; }
@@ -2091,18 +2164,22 @@ ul.pagination {
.dropdown-container ul li.padded {
padding: 6px 16px;
line-height: 1.2; }
.dropdown-container ul a {
.dropdown-container ul a, .dropdown-container ul button {
display: block;
padding: 6px 16px;
color: #555;
fill: #555; }
.dropdown-container ul a:hover {
fill: #555;
white-space: nowrap; }
.dropdown-container ul a:hover, .dropdown-container ul button:hover {
text-decoration: none;
background-color: #EEE; }
.dropdown-container ul a svg {
.dropdown-container ul a svg, .dropdown-container ul button svg {
margin-right: 12px;
display: inline-block;
width: 16px; }
.dropdown-container ul button {
width: 100%;
text-align: left; }
.dropdown-container ul li.border-bottom {
border-bottom: 1px solid #DDD; }
@@ -2123,29 +2200,6 @@ ul.pagination {
transform: scale(1.15);
opacity: .5; }
.book-grid-item .grid-card-content {
border-top: 0;
border-bottom-width: 2px; }
.book-grid-item .grid-card-content h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px; }
.book-grid-item .grid-card-content h2 a {
display: block;
width: 100%;
line-height: 1.2;
color: #009688;
fill: #009688;
text-decoration: none; }
.book-grid-item .grid-card-content p {
font-size: .85em;
margin: 0;
line-height: 1.6em; }
.book-grid-item .grid-card-footer p.small {
font-size: .8em;
margin: 0; }
.page-editor {
display: flex;
flex-direction: column;
@@ -2156,6 +2210,10 @@ ul.pagination {
.page-editor .edit-area {
flex: 1;
flex-direction: column; }
.page-editor .mce-tinymce {
box-shadow: none; }
.page-editor .mce-top-part::before {
box-shadow: none; }
.draft-notification {
pointer-events: none;
@@ -2174,10 +2232,13 @@ ul.pagination {
padding: 0 !important; }
.page-content {
width: 100%;
max-width: 840px;
margin: 0 auto;
margin-top: 48px;
overflow-wrap: break-word; }
.page-content.flex {
margin-top: 16px; }
.page-content .align-left {
text-align: left; }
.page-content img.align-left, .page-content table.align-left {
@@ -2212,6 +2273,8 @@ ul.pagination {
background: #dbffdb; }
.page-content del {
background: #FFECEC; }
.page-content.page-revision pre code {
white-space: pre-wrap; }
.pointer-container {
position: relative;
@@ -2228,8 +2291,10 @@ ul.pagination {
position: absolute;
top: -60px;
background-color: #FFF;
width: 272px;
width: 275px;
z-index: 55; }
.pointer.is-page-editable {
width: 328px; }
.pointer:before {
position: absolute;
left: 50%;
@@ -2253,12 +2318,13 @@ ul.pagination {
color: #666;
width: 172px;
z-index: 40; }
.pointer input, .pointer button {
.pointer input, .pointer button, .pointer a {
position: relative;
border-radius: 0;
height: 28px;
font-size: 12px;
vertical-align: top; }
vertical-align: top;
padding: 5px 16px; }
.pointer > i {
color: #888;
font-size: 18px;
@@ -2269,10 +2335,17 @@ ul.pagination {
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
.pointer .button {
.pointer .input-group .button {
line-height: 1;
margin: 0 0 0 -4px;
box-shadow: none; }
.pointer a.button {
margin: 0 0 0 0; }
.pointer a.button:hover {
fill: #fff; }
.pointer .svg-icon {
width: 1.2em;
height: 1.2em; }
.floating-toolbox {
background-color: #FFF;
@@ -2409,9 +2482,23 @@ ul.pagination {
.suggestion-box li.active {
background-color: #EEE; }
.comments-container {
width: 100%;
border-top: 1px solid #DDD;
margin-top: 32px;
margin-bottom: 16px; }
.comments-container h5 {
color: #888;
font-weight: normal;
margin-top: 0.5em; }
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
min-height: 175px; }
/* FIXME - Ugly hack to modify the media plugin for TinyMCE */
.mce-floatpanel[aria-label="Insert/edit media"] .mce-open {
display: none; }
body {
font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; }

355
public/dist/styles.css vendored
View File

@@ -144,6 +144,11 @@ a, .link, .text-button {
position: relative;
display: inline-block; }
.blended-links a {
color: inherit; }
.blended-links a svg {
fill: currentColor; }
/*
* Other HTML Text Elements
*/
@@ -284,6 +289,13 @@ p.secondary, p .secondary, span.secondary, .text-secondary {
color: #e27b41;
fill: #e27b41; }
.text-bookshelf {
color: #af5a5a;
fill: #af5a5a; }
.text-bookshelf:hover {
color: #af5a5a;
fill: #af5a5a; }
.text-book {
color: #009688;
fill: #009688; }
@@ -337,6 +349,7 @@ ul, ol {
ul {
padding-left: 20.8px;
padding-right: 20.8px;
list-style: disc; }
ul ul {
list-style: circle;
@@ -347,7 +360,8 @@ ul {
ol {
list-style: decimal;
padding-left: 32px; }
padding-left: 32px;
padding-right: 32px; }
li.checkbox-item, li.task-list-item {
list-style: none;
@@ -458,6 +472,8 @@ body.flexbox {
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh; }
.flex.sidebar section {
margin: 16px; }
.flex.sidebar + .flex.content {
flex: 3;
@@ -574,13 +590,40 @@ div[class^="col-"] img {
flex-direction: column;
border: 1px solid #ddd;
min-width: 100px; }
.grid-card h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px; }
.grid-card h2 a {
display: block;
width: 100%;
line-height: 1.2;
text-decoration: none; }
.grid-card p {
font-size: .85em;
margin: 0;
line-height: 1.6em; }
.grid-card .grid-card-content {
flex: 1; }
flex: 1;
border-top: 0;
border-bottom-width: 2px; }
.grid-card .grid-card-content, .grid-card .grid-card-footer {
padding: 24px; }
.grid-card .grid-card-content + .grid-card-footer {
padding-top: 0; }
.book-grid-item .grid-card-content h2 a {
color: #009688;
fill: #009688; }
.bookshelf-grid-item .grid-card-content h2 a {
color: #af5a5a;
fill: #af5a5a; }
.book-grid-item .grid-card-footer p.small {
font-size: .8em;
margin: 0; }
@media screen and (max-width: 800px) {
.grid.third {
grid-template-columns: 1fr 1fr; } }
@@ -1214,7 +1257,7 @@ div[class^="col-"] img {
display: block;
position: relative; }
.callout:before {
background-image: url("/icon/info-filled.svg?color=015380");
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+");
background-repeat: no-repeat;
content: '';
width: 1.2em;
@@ -1231,13 +1274,13 @@ div[class^="col-"] img {
background-color: #e7f3e7;
color: #376c39; }
.callout.success:before {
background-image: url("/icon/check-circle.svg?color=376c39"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+"); }
.callout.danger {
border-left-color: #E84F4F;
background-color: #fce8e8;
color: #b91818; }
.callout.danger:before {
background-image: url("/icon/danger.svg?color=b91818"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg=="); }
.callout.info {
border-left-color: #0288D1;
background-color: #d3efff;
@@ -1247,7 +1290,7 @@ div[class^="col-"] img {
background-color: #faeae0;
color: #b6531c; }
.callout.warning:before {
background-image: url("/icon/warning.svg?color=b6531c"); }
background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4="); }
.card {
margin: 16px;
@@ -1270,6 +1313,9 @@ div[class^="col-"] img {
word-wrap: break-word;
word-break: break-word; }
.sidebar .card h3, .sidebar .card .body, .sidebar .card .empty-text {
padding: 12px 16px; }
.card.drag-card {
border: 1px solid #DDD;
border-radius: 4px;
@@ -1287,6 +1333,7 @@ div[class^="col-"] img {
text-align: center;
justify-content: center;
width: 28px;
flex-grow: 0;
padding-left: 6px;
padding-right: 6px; }
.card.drag-card .handle:hover, .card.drag-card .drag-card-action:hover {
@@ -1294,7 +1341,8 @@ div[class^="col-"] img {
.card.drag-card .handle .svg-icon, .card.drag-card .drag-card-action .svg-icon {
margin-right: 0px; }
.card.drag-card > div .outline input {
margin: 12px 0; }
margin: 12px 0;
width: 100%; }
.card.drag-card > div.padded {
padding: 12px 0 !important; }
.card.drag-card .handle {
@@ -1305,13 +1353,38 @@ div[class^="col-"] img {
bottom: 0; }
.card.drag-card > div {
padding: 0 12px;
max-width: 80%; }
max-width: 80%;
flex: 1; }
.well {
background-color: #F8F8F8;
padding: 16px;
border: 1px solid #DDD; }
.tag-item {
display: inline-flex;
margin-bottom: 6px;
margin-right: 6px;
border-radius: 4px;
border: 1px solid #CCC;
overflow: hidden;
font-size: 0.85em; }
.tag-item a, .tag-item a:hover, .tag-item a:active {
padding: 4px 8px;
color: #777;
transition: background-color ease-in-out 80ms;
text-decoration: none; }
.tag-item a:hover {
background-color: rgba(255, 255, 255, 0.7); }
.tag-item svg {
fill: #888; }
.tag-item .tag-value {
border-left: 1px solid #DDD;
background-color: rgba(255, 255, 255, 0.5); }
.tag-list div:last-child .tag-item {
margin-bottom: 0; }
.button-base, .button, input[type="button"], input[type="submit"] {
text-decoration: none;
font-size: 15px;
@@ -1332,13 +1405,13 @@ div[class^="col-"] img {
text-transform: uppercase;
border: 1px solid #0288D1;
vertical-align: top; }
.button-base:hover, .button:hover, input[type="button"]:hover, input[type="submit"]:hover {
.button-base:hover, .button:hover, input:hover[type="button"], input:hover[type="submit"] {
background-color: #02a2f9;
text-decoration: none;
color: #EEE; }
.button-base:active, .button:active, input[type="button"]:active, input[type="submit"]:active {
.button-base:active, .button:active, input:active[type="button"], input:active[type="submit"] {
background-color: #026ea9; }
.button-base:focus, .button:focus, input[type="button"]:focus, input[type="submit"]:focus {
.button-base:focus, .button:focus, input:focus[type="button"], input:focus[type="submit"] {
background-color: #0295e5;
box-shadow: 0 0 4px 1px #CCC;
text-decoration: none;
@@ -1567,6 +1640,8 @@ table.table {
text-align: right; }
table.table .text-center {
text-align: center; }
table.table td.actions {
overflow: visible; }
table.no-style td {
border: 0;
@@ -1593,9 +1668,9 @@ table.list-table {
border: 1px solid #E84F4F; }
.input-base.pos, .pos.fake-input, input.pos[type="text"], input.pos[type="number"], input.pos[type="email"], input.pos[type="date"], input.pos[type="search"], input.pos[type="url"], input.pos[type="password"], select.pos, textarea.pos, .input-base.valid, .valid.fake-input, input.valid[type="text"], input.valid[type="number"], input.valid[type="email"], input.valid[type="date"], input.valid[type="search"], input.valid[type="url"], input.valid[type="password"], select.valid, textarea.valid {
border: 1px solid #52A256; }
.input-base.disabled, .disabled.fake-input, input.disabled[type="text"], input.disabled[type="number"], input.disabled[type="email"], input.disabled[type="date"], input.disabled[type="search"], input.disabled[type="url"], input.disabled[type="password"], select.disabled, textarea.disabled, .input-base[disabled], [disabled].fake-input, input[disabled][type="text"], input[disabled][type="number"], input[disabled][type="email"], input[disabled][type="date"], input[disabled][type="search"], input[disabled][type="url"], input[disabled][type="password"], select[disabled], textarea[disabled] {
.input-base.disabled, .disabled.fake-input, input.disabled[type="text"], input.disabled[type="number"], input.disabled[type="email"], input.disabled[type="date"], input.disabled[type="search"], input.disabled[type="url"], input.disabled[type="password"], select.disabled, textarea.disabled, .input-base[disabled], .fake-input[disabled], input[disabled][type="text"], input[disabled][type="number"], input[disabled][type="email"], input[disabled][type="date"], input[disabled][type="search"], input[disabled][type="url"], input[disabled][type="password"], select[disabled], textarea[disabled] {
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==); }
.input-base:focus, .fake-input:focus, input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="date"]:focus, input[type="search"]:focus, input[type="url"]:focus, input[type="password"]:focus, select:focus, textarea:focus {
.input-base:focus, .fake-input:focus, input:focus[type="text"], input:focus[type="number"], input:focus[type="email"], input:focus[type="date"], input:focus[type="search"], input:focus[type="url"], input:focus[type="password"], select:focus, textarea:focus {
outline: 0; }
.fake-input {
@@ -1776,6 +1851,8 @@ input:checked + .toggle-switch {
.form-group[collapsible].open .collapse-title label:before {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.form-group[collapsible] + .form-group[collapsible] {
margin-top: -13px; }
.inline-input-style, .title-input input[type="text"], .description-input textarea {
display: block;
@@ -2039,6 +2116,9 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
.page-content.mce-content-body p {
line-height: 1.6; }
.mce-colorbtn-trans {
overflow: hidden; }
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
@@ -2561,49 +2641,89 @@ span.CodeMirror-selectedtext {
width: 100%;
height: 100%; }
/**
* Custom Copy Button
*/
.CodeMirror-copy {
position: absolute;
top: -1px;
right: -1px;
background-color: #EEE;
padding: 6px;
line-height: 0;
border: 1px solid #DDD;
cursor: pointer;
fill: #444;
z-index: 5;
transition: all ease-in 240ms;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0;
pointer-events: none; }
.CodeMirror-copy svg {
transition: -webkit-transform ease-in 240ms;
transition: transform ease-in 240ms;
transition: transform ease-in 240ms, -webkit-transform ease-in 240ms;
-webkit-transform: translateY(0);
transform: translateY(0); }
.CodeMirror-copy.success {
background-color: #70b774;
fill: #FFF; }
.CodeMirror-copy.success svg {
-webkit-transform: translateY(-3px);
transform: translateY(-3px); }
.CodeMirror:hover .CodeMirror-copy {
-webkit-user-select: all;
-moz-user-select: all;
-ms-user-select: all;
user-select: all;
opacity: 1;
pointer-events: all; }
[notification] {
position: fixed;
top: 0;
right: 0;
margin: 64px 32px;
padding: 24px 32px;
padding: 16px 24px;
background-color: #EEE;
border-radius: 3px;
box-shadow: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
box-shadow: 0 1px 3px 1px rgba(76, 76, 76, 0.26), 0 1px 12px 0px rgba(76, 76, 76, 0.2);
z-index: 999999;
cursor: pointer;
max-width: 360px;
transition: -webkit-transform ease-in-out 280ms;
transition: transform ease-in-out 280ms;
transition: transform ease-in-out 280ms, -webkit-transform ease-in-out 280ms;
-webkit-transform: translate3d(580px, 0, 0);
transform: translate3d(580px, 0, 0);
-webkit-transform: translateX(580px);
transform: translateX(580px);
display: grid;
grid-template-columns: 64px 1fr; }
grid-template-columns: 42px 1fr;
color: #FFF; }
[notification] span, [notification] svg {
vertical-align: middle;
justify-self: center;
align-self: center; }
[notification] svg {
fill: #EEEEEE;
width: 4em;
height: 4em;
padding-right: 16px; }
width: 2.8rem;
height: 2.8rem;
padding-right: 12px; }
[notification] span {
vertical-align: middle;
line-height: 1.3; }
[notification].pos {
background-color: #52A256;
color: #EEE; }
background-color: #52A256; }
[notification].neg {
background-color: #E84F4F;
color: #EEE; }
background-color: #E84F4F; }
[notification].warning {
background-color: #e27b41;
color: #EEE; }
background-color: #e27b41; }
[notification].showing {
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0); }
-webkit-transform: translateX(0);
transform: translateX(0); }
[notification].showing:hover {
-webkit-transform: translate3d(0, -2px, 0);
transform: translate3d(0, -2px, 0); }
@@ -2697,7 +2817,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.dropzone-container {
position: relative;
border: 3px dashed #DDD; }
background-color: #EEE;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%23a9a9a9' fill-opacity='0.52' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); }
.image-manager-list .image {
display: block;
@@ -2709,13 +2830,13 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
width: 16.66667%;
height: auto;
border: 1px solid #DDD;
box-shadow: 0 0 0 0 transparent;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
transition: all cubic-bezier(0.4, 0, 1, 1) 160ms;
overflow: hidden; }
.image-manager-list .image.selected {
-webkit-transform: scale3d(0.92, 0.92, 0.92);
transform: scale3d(0.92, 0.92, 0.92);
border: 1px solid #444;
border: 4px solid #FFF;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2); }
.image-manager-list .image img {
width: 100%;
@@ -2753,12 +2874,26 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin-left: 1px;
padding: 16px 24px;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #DDD; }
.image-manager-sidebar .inner {
padding: 16px; }
.image-manager-sidebar img {
max-width: 100%;
max-height: 180px;
display: block;
margin: 0 auto 16px auto;
box-shadow: 0 1px 21px 1px rgba(76, 76, 76, 0.3); }
.image-manager-sidebar .image-manager-viewer {
height: 196px;
display: flex;
align-items: center;
justify-content: center; }
.image-manager-sidebar .image-manager-viewer a {
display: inline-block; }
.image-manager-sidebar .dropzone-container {
margin-top: 16px; }
border-bottom: 1px solid #DDD; }
.image-manager-list {
overflow-y: scroll;
@@ -2778,10 +2913,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
* Copyright (c) 2012 Matias Meno <m@tias.me>
*/
.dz-message {
font-size: 1.2em;
line-height: 1.1;
font-size: 1em;
line-height: 2.35;
font-style: italic;
color: #aaa;
color: #888;
text-align: center;
cursor: pointer;
padding: 24px 16px;
@@ -3087,6 +3222,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
height: 200px; } }
.comment-box {
clear: left;
border: 1px solid #DDD;
margin-bottom: 12px;
border-radius: 3px; }
@@ -3117,6 +3253,9 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.comment-box .header .text-muted {
color: #999; }
#tag-manager .drag-card {
max-width: 500px; }
/**
* Includes the main navigation header and the faded toolbar.
*/
@@ -3133,20 +3272,21 @@ header {
header .links {
display: inline-block;
vertical-align: top;
margin-right: 32px; }
@media screen and (max-width: 992px) {
header .links {
margin-right: 16px; } }
margin-left: 16px; }
header .links a {
display: inline-block;
padding: 16px 24px;
padding: 16px;
color: #FFF;
fill: #FFF; }
header .links a:last-child {
padding-right: 0; }
@media screen and (max-width: 992px) {
header .links a {
padding: 16px 12px; } }
header .dropdown-container {
padding-left: 16px;
padding-right: 0; }
@media screen and (max-width: 992px) {
header .links a {
padding-left: 12px;
padding-right: 12px; }
header .dropdown-container {
padding-left: 12px; } }
header .avatar, header .user-name {
display: inline-block; }
header .avatar {
@@ -3192,10 +3332,13 @@ header .search-box {
header .search-box input {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #EEE; }
header .search-box button {
color: #EEE;
fill: #EEE; }
z-index: 2; }
header .search-box button {
fill: #EEE;
z-index: 1; }
header .search-box button svg {
margin-right: 0; }
header .search-box ::-webkit-input-placeholder {
/* Chrome/Opera/Safari */
color: #DDD; }
@@ -3463,7 +3606,7 @@ header .search-box {
.book-tree .sidebar-page-list .sub-menu {
display: none;
padding-left: 0; }
.book-tree .sidebar-page-list .sub-menu.open {
.book-tree .sidebar-page-list [chapter-toggle].open + .sub-menu {
display: block; }
.sortable-page-list, .sortable-page-list ul {
@@ -3530,8 +3673,6 @@ ul.pagination {
padding: 3px 12px;
border: 1px solid #CCC;
margin-left: -1px;
color: #888;
fill: #888;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@@ -3539,13 +3680,7 @@ ul.pagination {
ul.pagination a.disabled, ul.pagination span.disabled {
cursor: not-allowed; }
ul.pagination li.active span {
background-color: rgba(2, 136, 209, 0.8);
color: #EEE;
fill: #EEE;
border-color: rgba(2, 136, 209, 0.8); }
ul.pagination a {
color: #0288D1;
fill: #0288D1; }
color: #FFF; }
.compact ul.pagination {
margin: 0; }
@@ -3622,18 +3757,22 @@ ul.pagination {
.dropdown-container ul li.padded {
padding: 6px 16px;
line-height: 1.2; }
.dropdown-container ul a {
.dropdown-container ul a, .dropdown-container ul button {
display: block;
padding: 6px 16px;
color: #555;
fill: #555; }
.dropdown-container ul a:hover {
fill: #555;
white-space: nowrap; }
.dropdown-container ul a:hover, .dropdown-container ul button:hover {
text-decoration: none;
background-color: #EEE; }
.dropdown-container ul a svg {
.dropdown-container ul a svg, .dropdown-container ul button svg {
margin-right: 12px;
display: inline-block;
width: 16px; }
.dropdown-container ul button {
width: 100%;
text-align: left; }
.dropdown-container ul li.border-bottom {
border-bottom: 1px solid #DDD; }
@@ -3654,29 +3793,6 @@ ul.pagination {
transform: scale(1.15);
opacity: .5; }
.book-grid-item .grid-card-content {
border-top: 0;
border-bottom-width: 2px; }
.book-grid-item .grid-card-content h2 {
width: 100%;
font-size: 1.5em;
margin: 0 0 10px; }
.book-grid-item .grid-card-content h2 a {
display: block;
width: 100%;
line-height: 1.2;
color: #009688;
fill: #009688;
text-decoration: none; }
.book-grid-item .grid-card-content p {
font-size: .85em;
margin: 0;
line-height: 1.6em; }
.book-grid-item .grid-card-footer p.small {
font-size: .8em;
margin: 0; }
.page-editor {
display: flex;
flex-direction: column;
@@ -3687,6 +3803,10 @@ ul.pagination {
.page-editor .edit-area {
flex: 1;
flex-direction: column; }
.page-editor .mce-tinymce {
box-shadow: none; }
.page-editor .mce-top-part::before {
box-shadow: none; }
.draft-notification {
pointer-events: none;
@@ -3705,10 +3825,13 @@ ul.pagination {
padding: 0 !important; }
.page-content {
width: 100%;
max-width: 840px;
margin: 0 auto;
margin-top: 48px;
overflow-wrap: break-word; }
.page-content.flex {
margin-top: 16px; }
.page-content .align-left {
text-align: left; }
.page-content img.align-left, .page-content table.align-left {
@@ -3743,6 +3866,8 @@ ul.pagination {
background: #dbffdb; }
.page-content del {
background: #FFECEC; }
.page-content.page-revision pre code {
white-space: pre-wrap; }
.pointer-container {
position: relative;
@@ -3759,8 +3884,10 @@ ul.pagination {
position: absolute;
top: -60px;
background-color: #FFF;
width: 272px;
width: 275px;
z-index: 55; }
.pointer.is-page-editable {
width: 328px; }
.pointer:before {
position: absolute;
left: 50%;
@@ -3784,12 +3911,13 @@ ul.pagination {
color: #666;
width: 172px;
z-index: 40; }
.pointer input, .pointer button {
.pointer input, .pointer button, .pointer a {
position: relative;
border-radius: 0;
height: 28px;
font-size: 12px;
vertical-align: top; }
vertical-align: top;
padding: 5px 16px; }
.pointer > i {
color: #888;
font-size: 18px;
@@ -3800,10 +3928,17 @@ ul.pagination {
-moz-user-select: none;
-ms-user-select: none;
user-select: none; }
.pointer .button {
.pointer .input-group .button {
line-height: 1;
margin: 0 0 0 -4px;
box-shadow: none; }
.pointer a.button {
margin: 0 0 0 0; }
.pointer a.button:hover {
fill: #fff; }
.pointer .svg-icon {
width: 1.2em;
height: 1.2em; }
.floating-toolbox {
background-color: #FFF;
@@ -3940,9 +4075,23 @@ ul.pagination {
.suggestion-box li.active {
background-color: #EEE; }
.comments-container {
width: 100%;
border-top: 1px solid #DDD;
margin-top: 32px;
margin-bottom: 16px; }
.comments-container h5 {
color: #888;
font-weight: normal;
margin-top: 0.5em; }
.comment-editor .CodeMirror, .comment-editor .CodeMirror-scroll {
min-height: 175px; }
/* FIXME - Ugly hack to modify the media plugin for TinyMCE */
.mce-floatpanel[aria-label="Insert/edit media"] .mce-open {
display: none; }
[v-cloak] {
display: none;
opacity: 0;
@@ -4045,8 +4194,6 @@ body.dragging, body.dragging * {
[back-to-top]:hover {
width: 136px;
opacity: 1 !important; }
[back-to-top]:hover span {
display: inline-block; }
[back-to-top] .inner {
width: 136px; }
[back-to-top] span {
@@ -4061,7 +4208,8 @@ body.dragging, body.dragging * {
border: 1px solid #DDD;
margin-left: -1px; }
.contained-search-box input {
flex: 5; }
flex: 5;
padding: 6px 12px; }
.contained-search-box input:focus, .contained-search-box input:active {
outline: 0; }
.contained-search-box button {
@@ -4102,10 +4250,25 @@ body.dragging, body.dragging * {
background-color: #FFF;
transition: all ease-in-out 120ms;
cursor: pointer; }
.entity-selector.compact {
font-size: 10px; }
.entity-selector.compact .entity-item-snippet {
display: none; }
.entity-list-item.selected h3, .entity-list-item.selected i, .entity-list-item.selected p, .entity-list-item.selected a, .entity-list-item.selected span {
color: #EEE; }
.scroll-box {
max-height: 250px;
overflow-y: scroll;
border: 1px solid #DDD;
border-radius: 3px; }
.scroll-box .scroll-box-item {
padding: 6px 16px;
border-bottom: 1px solid #DDD; }
.scroll-box .scroll-box-item:last-child {
border-bottom: 0; }
.center-box {
margin: 48px auto 0 auto;
width: 420px;

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("advlist",function(a){function c(b){return a.$.contains(a.getBody(),b)}function d(a){return a&&/^(OL|UL|DL)$/.test(a.nodeName)&&c(a)}function e(a,c){var d=[];return c&&b.each(c.split(/[ ,]/),function(a){d.push({text:a.replace(/\-/g," ").replace(/\b\w/g,function(a){return a.toUpperCase()}),data:"default"==a?"":a})}),d}function f(b,c){var d="UL"==b?"InsertUnorderedList":"InsertOrderedList";a.execCommand(d,!1,c===!1?null:{"list-style-type":c})}function g(b){var c=a.dom.getStyle(a.dom.getParent(a.selection.getNode(),"ol,ul"),"listStyleType")||"";b.control.items().each(function(a){a.active(a.settings.data===c)})}var h,i,j=function(a,c){var d=a.settings.plugins?a.settings.plugins:"";return b.inArray(d.split(/[ ,]/),c)!==-1};h=e("OL",a.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman")),i=e("UL",a.getParam("advlist_bullet_styles","default,circle,disc,square"));var k=function(c){return function(){var e=this;a.on("NodeChange",function(a){var f=b.grep(a.parents,d);e.active(f.length>0&&f[0].nodeName===c)})}};j(a,"lists")&&(a.addCommand("ApplyUnorderedListStyle",function(a,b){f("UL",b["list-style-type"])}),a.addCommand("ApplyOrderedListStyle",function(a,b){f("OL",b["list-style-type"])}),a.addButton("numlist",{type:h.length>0?"splitbutton":"button",tooltip:"Numbered list",menu:h,onPostRender:k("OL"),onshow:g,onselect:function(a){f("OL",a.control.settings.data)},onclick:function(){a.execCommand("InsertOrderedList")}}),a.addButton("bullist",{type:i.length>0?"splitbutton":"button",tooltip:"Bullet list",onPostRender:k("UL"),menu:i,onshow:g,onselect:function(a){f("UL",a.control.settings.data)},onclick:function(){a.execCommand("InsertUnorderedList")}}))}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.Tools"),s=function(t,e,n){var r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===n?null:{"list-style-type":n})},o=function(n){n.addCommand("ApplyUnorderedListStyle",function(t,e){s(n,"UL",e["list-style-type"])}),n.addCommand("ApplyOrderedListStyle",function(t,e){s(n,"OL",e["list-style-type"])})},e=function(t){var e=t.getParam("advlist_number_styles","default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman");return e?e.split(/[ ,]/):[]},n=function(t){var e=t.getParam("advlist_bullet_styles","default,circle,disc,square");return e?e.split(/[ ,]/):[]},u=function(t){return t&&/^(TH|TD)$/.test(t.nodeName)},c=function(r){return function(t){return t&&/^(OL|UL|DL)$/.test(t.nodeName)&&(n=t,(e=r).$.contains(e.getBody(),n));var e,n}},d=function(t){var e=t.dom.getParent(t.selection.getNode(),"ol,ul");return t.dom.getStyle(e,"listStyleType")||""},p=function(t){return a.map(t,function(t){return{text:t.replace(/\-/g," ").replace(/\b\w/g,function(t){return t.toUpperCase()}),data:"default"===t?"":t}})},f=function(i,l){return function(t){var o=t.control;i.on("NodeChange",function(t){var e=function(t,e){for(var n=0;n<t.length;n++)if(e(t[n]))return n;return-1}(t.parents,u),n=-1!==e?t.parents.slice(0,e):t.parents,r=a.grep(n,c(i));o.active(0<r.length&&r[0].nodeName===l)})}},m=function(e,t,n,r,o,i){var l;e.addButton(t,{active:!1,type:"splitbutton",tooltip:n,menu:p(i),onPostRender:f(e,o),onshow:(l=e,function(t){var e=d(l);t.control.items().each(function(t){t.active(t.settings.data===e)})}),onselect:function(t){s(e,o,t.control.settings.data)},onclick:function(){e.execCommand(r)}})},r=function(t,e,n,r,o,i){var l,a,s,u,c;0<i.length?m(t,e,n,r,o,i):(a=e,s=n,u=r,c=o,(l=t).addButton(a,{active:!1,type:"button",tooltip:s,onPostRender:f(l,c),onclick:function(){l.execCommand(u)}}))},i=function(t){r(t,"numlist","Numbered list","InsertOrderedList","OL",e(t)),r(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))};t.add("advlist",function(t){var e,n,r;n="lists",r=(e=t).settings.plugins?e.settings.plugins:"",-1!==a.inArray(r.split(/[ ,]/),n)&&(i(t),o(t))})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("anchor",function(b){var c=function(a){return!a.attr("href")&&(a.attr("id")||a.attr("name"))&&!a.firstChild},d=function(a){return function(b){for(var d=0;d<b.length;d++)c(b[d])&&b[d].attr("contenteditable",a)}},e=function(a){return/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(a)},f=function(){var a=b.selection.getNode(),c="A"==a.tagName&&""===b.dom.getAttrib(a,"href"),d="";c&&(d=a.id||a.name||""),b.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:d},onsubmit:function(d){var f=d.data.id;return e(f)?void(c?(a.removeAttribute("name"),a.id=f):(b.selection.collapse(!0),b.execCommand("mceInsertContent",!1,b.dom.createHTML("a",{id:f})))):(d.preventDefault(),void b.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."))}})};a.ceFalse&&b.on("PreInit",function(){b.parser.addNodeFilter("a",d("false")),b.serializer.addNodeFilter("a",d(null))}),b.addCommand("mceAnchor",f),b.addButton("anchor",{icon:"anchor",tooltip:"Anchor",onclick:f,stateSelector:"a:not([href])"}),b.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",onclick:f})}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(t)},e=function(t){var e=t.selection.getNode();return"A"===e.tagName&&""===t.dom.getAttrib(e,"href")?e.id||e.name:""},i=function(t,e){var n=t.selection.getNode();"A"===n.tagName&&""===t.dom.getAttrib(n,"href")?(n.removeAttribute("name"),n.id=e,t.undoManager.add()):(t.focus(),t.selection.collapse(!0),t.execCommand("mceInsertContent",!1,t.dom.createHTML("a",{id:e})))},n=function(r){var t=e(r);r.windowManager.open({title:"Anchor",body:{type:"textbox",name:"id",size:40,label:"Id",value:t},onsubmit:function(t){var e,n,o=t.data.id;e=r,(a(n=o)?(i(e,n),0):(e.windowManager.alert("Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores."),1))&&t.preventDefault()}})},o=function(t){t.addCommand("mceAnchor",function(){n(t)})},r=function(o){return function(t){for(var e=0;e<t.length;e++)(n=t[e]).attr("href")||!n.attr("id")&&!n.attr("name")||n.firstChild||t[e].attr("contenteditable",o);var n}},c=function(t){t.on("PreInit",function(){t.parser.addNodeFilter("a",r("false")),t.serializer.addNodeFilter("a",r(null))})},d=function(t){t.addButton("anchor",{icon:"anchor",tooltip:"Anchor",cmd:"mceAnchor",stateSelector:"a:not([href])"}),t.addMenuItem("anchor",{icon:"anchor",text:"Anchor",context:"insert",cmd:"mceAnchor"})};t.add("anchor",function(t){c(t),o(t),d(t)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.Env")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){var c=function(a,b){return a===b||" "===a||160===a.charCodeAt(0)};return b.add("autolink",function(b){function d(a){g(a,-1,"(",!0)}function e(a){g(a,0,"",!0)}function f(a){g(a,-1,"",!1)}function g(a,b,d){function e(a,b){if(b<0&&(b=0),3==a.nodeType){var c=a.data.length;b>c&&(b=c)}return b}function f(a,b){1!=a.nodeType||a.hasChildNodes()?h.setStart(a,e(a,b)):h.setStartBefore(a)}function g(a,b){1!=a.nodeType||a.hasChildNodes()?h.setEnd(a,e(a,b)):h.setEndAfter(a)}var h,j,k,l,m,n,o,p,q,r;if("A"!=a.selection.getNode().tagName){if(h=a.selection.getRng(!0).cloneRange(),h.startOffset<5){if(p=h.endContainer.previousSibling,!p){if(!h.endContainer.firstChild||!h.endContainer.firstChild.nextSibling)return;p=h.endContainer.firstChild.nextSibling}if(q=p.length,f(p,q),g(p,q),h.endOffset<5)return;j=h.endOffset,l=p}else{if(l=h.endContainer,3!=l.nodeType&&l.firstChild){for(;3!=l.nodeType&&l.firstChild;)l=l.firstChild;3==l.nodeType&&(f(l,0),g(l,l.nodeValue.length))}j=1==h.endOffset?2:h.endOffset-1-b}k=j;do f(l,j>=2?j-2:0),g(l,j>=1?j-1:0),j-=1,r=h.toString();while(" "!=r&&""!==r&&160!=r.charCodeAt(0)&&j-2>=0&&r!=d);c(h.toString(),d)?(f(l,j),g(l,k),j+=1):0===h.startOffset?(f(l,0),g(l,k)):(f(l,j),g(l,k)),n=h.toString(),"."==n.charAt(n.length-1)&&g(l,k-1),n=h.toString(),o=n.match(i),o&&("www."==o[1]?o[1]="http://www.":/@$/.test(o[1])&&!/^mailto:/.test(o[1])&&(o[1]="mailto:"+o[1]),m=a.selection.getBookmark(),a.selection.setRng(h),a.execCommand("createlink",!1,o[1]+o[2]),a.settings.default_link_target&&a.dom.setAttrib(a.selection.getNode(),"target",a.settings.default_link_target),a.selection.moveToBookmark(m),a.nodeChanged())}}var h,i=/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i;return b.settings.autolink_pattern&&(i=b.settings.autolink_pattern),b.on("keydown",function(a){if(13==a.keyCode)return f(b)}),a.ie?void b.on("focus",function(){if(!h){h=!0;try{b.execCommand("AutoUrlDetect",!1,!0)}catch(a){}}}):(b.on("keypress",function(a){if(41==a.keyCode)return d(b)}),void b.on("keyup",function(a){if(32==a.keyCode)return e(b)}))}),function(){}}),d("0")()}();
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=tinymce.util.Tools.resolve("tinymce.Env"),m=function(e){return e.getParam("autolink_pattern",/^(https?:\/\/|ssh:\/\/|ftp:\/\/|file:\/|www\.|(?:mailto:)?[A-Z0-9._%+\-]+@)(.+)$/i)},y=function(e){return e.getParam("default_link_target","")},o=function(e,t){if(t<0&&(t=0),3===e.nodeType){var n=e.data.length;n<t&&(t=n)}return t},k=function(e,t,n){1!==t.nodeType||t.hasChildNodes()?e.setStart(t,o(t,n)):e.setStartBefore(t)},p=function(e,t,n){1!==t.nodeType||t.hasChildNodes()?e.setEnd(t,o(t,n)):e.setEndAfter(t)},r=function(e,t,n){var i,o,r,a,f,s,d,l,c,u,g=m(e),h=y(e);if("A"!==e.selection.getNode().tagName){if((i=e.selection.getRng(!0).cloneRange()).startOffset<5){if(!(l=i.endContainer.previousSibling)){if(!i.endContainer.firstChild||!i.endContainer.firstChild.nextSibling)return;l=i.endContainer.firstChild.nextSibling}if(c=l.length,k(i,l,c),p(i,l,c),i.endOffset<5)return;o=i.endOffset,a=l}else{if(3!==(a=i.endContainer).nodeType&&a.firstChild){for(;3!==a.nodeType&&a.firstChild;)a=a.firstChild;3===a.nodeType&&(k(i,a,0),p(i,a,a.nodeValue.length))}o=1===i.endOffset?2:i.endOffset-1-t}for(r=o;k(i,a,2<=o?o-2:0),p(i,a,1<=o?o-1:0),o-=1," "!==(u=i.toString())&&""!==u&&160!==u.charCodeAt(0)&&0<=o-2&&u!==n;);var C;(C=i.toString())===n||" "===C||160===C.charCodeAt(0)?(k(i,a,o),p(i,a,r),o+=1):(0===i.startOffset?k(i,a,0):k(i,a,o),p(i,a,r)),"."===(s=i.toString()).charAt(s.length-1)&&p(i,a,r-1),(d=(s=i.toString().trim()).match(g))&&("www."===d[1]?d[1]="http://www.":/@$/.test(d[1])&&!/^mailto:/.test(d[1])&&(d[1]="mailto:"+d[1]),f=e.selection.getBookmark(),e.selection.setRng(i),e.execCommand("createlink",!1,d[1]+d[2]),h&&e.dom.setAttrib(e.selection.getNode(),"target",h),e.selection.moveToBookmark(f),e.nodeChanged())}},t=function(t){var n;t.on("keydown",function(e){13!==e.keyCode||r(t,-1,"")}),i.ie?t.on("focus",function(){if(!n){n=!0;try{t.execCommand("AutoUrlDetect",!1,!0)}catch(e){}}}):(t.on("keypress",function(e){41!==e.keyCode||r(t,-1,"(")}),t.on("keyup",function(e){32!==e.keyCode||r(t,0,"")}))};e.add("autolink",function(e){t(e)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("5",tinymce.util.Tools.resolve),g("1",["5"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["5"],function(a){return a("tinymce.Env")}),g("3",["5"],function(a){return a("tinymce.PluginManager")}),g("4",["5"],function(a){return a("tinymce.util.Delay")}),g("0",["1","2","3","4"],function(a,b,c,d){var e=a.DOM;return c.add("autoresize",function(a){function c(){return a.plugins.fullscreen&&a.plugins.fullscreen.isFullscreen()}function f(d){var g,j,k,l,m,n,o,p,q,r,s,t;if(j=a.getDoc()){if(k=j.body,l=j.documentElement,m=h.autoresize_min_height,!k||d&&"setcontent"===d.type&&d.initial||c())return void(k&&l&&(k.style.overflowY="auto",l.style.overflowY="auto"));o=a.dom.getStyle(k,"margin-top",!0),p=a.dom.getStyle(k,"margin-bottom",!0),q=a.dom.getStyle(k,"padding-top",!0),r=a.dom.getStyle(k,"padding-bottom",!0),s=a.dom.getStyle(k,"border-top-width",!0),t=a.dom.getStyle(k,"border-bottom-width",!0),n=k.offsetHeight+parseInt(o,10)+parseInt(p,10)+parseInt(q,10)+parseInt(r,10)+parseInt(s,10)+parseInt(t,10),(isNaN(n)||n<=0)&&(n=b.ie?k.scrollHeight:b.webkit&&0===k.clientHeight?0:k.offsetHeight),n>h.autoresize_min_height&&(m=n),h.autoresize_max_height&&n>h.autoresize_max_height?(m=h.autoresize_max_height,k.style.overflowY="auto",l.style.overflowY="auto"):(k.style.overflowY="hidden",l.style.overflowY="hidden",k.scrollTop=0),m!==i&&(g=m-i,e.setStyle(a.iframeElement,"height",m+"px"),i=m,b.webKit&&g<0&&f(d))}}function g(b,c,e){d.setEditorTimeout(a,function(){f({}),b--?g(b,c,e):e&&e()},c)}var h=a.settings,i=0;a.settings.inline||(h.autoresize_min_height=parseInt(a.getParam("autoresize_min_height",a.getElement().offsetHeight),10),h.autoresize_max_height=parseInt(a.getParam("autoresize_max_height",0),10),a.on("init",function(){var b,c;b=a.getParam("autoresize_overflow_padding",1),c=a.getParam("autoresize_bottom_margin",50),b!==!1&&a.dom.setStyles(a.getBody(),{paddingLeft:b,paddingRight:b}),c!==!1&&a.dom.setStyles(a.getBody(),{paddingBottom:c})}),a.on("nodechange setcontent keyup FullscreenStateChanged",f),a.getParam("autoresize_on_init",!0)&&a.on("init",function(){g(20,100,function(){g(5,1e3)})}),a.addCommand("mceAutoResize",f))}),function(){}}),d("0")()}();
!function(){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),y=tinymce.util.Tools.resolve("tinymce.Env"),r=tinymce.util.Tools.resolve("tinymce.util.Delay"),h=function(t){return parseInt(t.getParam("autoresize_min_height",t.getElement().offsetHeight),10)},v=function(t){return parseInt(t.getParam("autoresize_max_height",0),10)},o=function(t){return t.getParam("autoresize_overflow_padding",1)},a=function(t){return t.getParam("autoresize_bottom_margin",50)},n=function(t){return t.getParam("autoresize_on_init",!0)},u=function(t,e,n,i,o){r.setEditorTimeout(t,function(){_(t,e),n--?u(t,e,n,i,o):o&&o()},i)},S=function(t,e){var n=t.getBody();n&&(n.style.overflowY=e?"":"hidden",e||(n.scrollTop=0))},_=function(t,e){var n,i,o,r,a,u,s,l,g,c,f,d=t.dom;if(i=t.getDoc())if((m=t).plugins.fullscreen&&m.plugins.fullscreen.isFullscreen())S(t,!0);else{var m;o=i.body,r=h(t),u=d.getStyle(o,"margin-top",!0),s=d.getStyle(o,"margin-bottom",!0),l=d.getStyle(o,"padding-top",!0),g=d.getStyle(o,"padding-bottom",!0),c=d.getStyle(o,"border-top-width",!0),f=d.getStyle(o,"border-bottom-width",!0),a=o.offsetHeight+parseInt(u,10)+parseInt(s,10)+parseInt(l,10)+parseInt(g,10)+parseInt(c,10)+parseInt(f,10),(isNaN(a)||a<=0)&&(a=y.ie?o.scrollHeight:y.webkit&&0===o.clientHeight?0:o.offsetHeight),a>h(t)&&(r=a);var p=v(t);p&&p<a?(r=p,S(t,!0)):S(t,!1),r!==e.get()&&(n=r-e.get(),d.setStyle(t.iframeElement,"height",r+"px"),e.set(r),y.webkit&&n<0&&_(t,e))}},s={setup:function(i,e){i.on("init",function(){var t,e,n=i.dom;t=o(i),e=a(i),!1!==t&&n.setStyles(i.getBody(),{paddingLeft:t,paddingRight:t}),!1!==e&&n.setStyles(i.getBody(),{paddingBottom:e})}),i.on("nodechange setcontent keyup FullscreenStateChanged",function(t){_(i,e)}),n(i)&&i.on("init",function(){u(i,e,20,100,function(){u(i,e,5,1e3)})})},resize:_},l=function(t,e){t.addCommand("mceAutoResize",function(){s.resize(t,e)})};t.add("autoresize",function(t){if(!t.inline){var e=i(0);l(t,e),s.setup(t,e)}})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.EditorManager")}),g("2",["6"],function(a){return a("tinymce.PluginManager")}),g("3",["6"],function(a){return a("tinymce.util.LocalStorage")}),g("4",["6"],function(a){return a("tinymce.util.Tools")}),h("5",window),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return a._beforeUnloadHandler=function(){var b;return d.each(a.get(),function(a){a.plugins.autosave&&a.plugins.autosave.storeDraft(),!b&&a.isDirty()&&a.getParam("autosave_ask_before_unload",!0)&&(b=a.translate("You have unsaved changes are you sure you want to navigate away?"))}),b},b.add("autosave",function(b){function f(a,b){var c={s:1e3,m:6e4};return a=/^(\d+)([ms]?)$/.exec(""+(a||b)),(a[2]?c[a[2]]:1)*parseInt(a,10)}function g(){var a=parseInt(c.getItem(o+"time"),10)||0;return!((new Date).getTime()-a>q.autosave_retention)||(h(!1),!1)}function h(a){c.removeItem(o+"draft"),c.removeItem(o+"time"),a!==!1&&b.fire("RemoveDraft")}function i(){!n()&&b.isDirty()&&(c.setItem(o+"draft",b.getContent({format:"raw",no_events:!0})),c.setItem(o+"time",(new Date).getTime()),b.fire("StoreDraft"))}function j(){g()&&(b.setContent(c.getItem(o+"draft"),{format:"raw"}),b.fire("RestoreDraft"))}function k(){p||(setInterval(function(){b.removed||i()},q.autosave_interval),p=!0)}function l(){var a=this;a.disabled(!g()),b.on("StoreDraft RestoreDraft RemoveDraft",function(){a.disabled(!g())}),k()}function m(){b.undoManager.beforeChange(),j(),h(),b.undoManager.add()}function n(a){var c=b.settings.forced_root_block;return a=d.trim("undefined"==typeof a?b.getBody().innerHTML:a),""===a||new RegExp("^<"+c+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+c+">|<br>$","i").test(a)}var o,p,q=b.settings;o=q.autosave_prefix||"tinymce-autosave-{path}{query}-{id}-",o=o.replace(/\{path\}/g,document.location.pathname),o=o.replace(/\{query\}/g,document.location.search),o=o.replace(/\{id\}/g,b.id),q.autosave_interval=f(q.autosave_interval,"30s"),q.autosave_retention=f(q.autosave_retention,"20m"),b.addButton("restoredraft",{title:"Restore last draft",onclick:m,onPostRender:l}),b.addMenuItem("restoredraft",{text:"Restore last draft",onclick:m,onPostRender:l,context:"file"}),b.settings.autosave_restore_when_empty!==!1&&(b.on("init",function(){g()&&n()&&j()}),b.on("saveContent",function(){h()})),e.onbeforeunload=a._beforeUnloadHandler,this.hasDraft=g,this.storeDraft=i,this.restoreDraft=j,this.removeDraft=h,this.isEmpty=n}),function(){}}),d("0")()}();
!function(){"use strict";var n=function(t){var e=t,r=function(){return e};return{get:r,set:function(t){e=t},clone:function(){return n(r())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),r=function(t){return t.fire("RestoreDraft")},i=function(t){return t.fire("StoreDraft")},s=function(t){return t.fire("RemoveDraft")},e=function(t,e){return((t=/^(\d+)([ms]?)$/.exec(""+(t||e)))[2]?{s:1e3,m:6e4}[t[2]]:1)*parseInt(t,10)},u=function(t){return t.getParam("autosave_ask_before_unload",!0)},f=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t){return e(t.settings.autosave_interval,"30s")},l=function(t){return e(t.settings.autosave_retention,"20m")},m=function(t,e){var r=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+r+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+r+">|<br>$","i").test(e)},v=function(t){var e=parseInt(a.getItem(f(t)+"time"),10)||0;return!((new Date).getTime()-e>l(t)&&(d(t,!1),1))},d=function(t,e){var r=f(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&s(t)},D=function(t){var e=f(t);!m(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),i(t))},g=function(t){var e=f(t);v(t)&&(t.setContent(a.getItem(e+"draft"),{format:"raw"}),r(t))},y={isEmpty:m,hasDraft:v,removeDraft:d,storeDraft:D,restoreDraft:g,startStoreDraft:function(t,e){var r=c(t);e.get()||(setInterval(function(){t.removed||D(t)},r),e.set(!0))},restoreLastDraft:function(t){t.undoManager.transact(function(){g(t),d(t)}),t.focus()}},p=function(e,r){return function(){var t=Array.prototype.slice.call(arguments);return e.apply(null,[r].concat(t))}},h=function(t){return{hasDraft:p(y.hasDraft,t),storeDraft:p(y.storeDraft,t),restoreDraft:p(y.restoreDraft,t),removeDraft:p(y.removeDraft,t),isEmpty:p(y.isEmpty,t)}},_=tinymce.util.Tools.resolve("tinymce.EditorManager");_._beforeUnloadHandler=function(){var e;return o.each(_.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var b=function(t){window.onbeforeunload=_._beforeUnloadHandler},I=function(r,n){return function(t){var e=t.control;e.disabled(!y.hasDraft(r)),r.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!y.hasDraft(r))}),y.startStoreDraft(r,n)}},w=function(t,e){t.addButton("restoredraft",{title:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e)}),t.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e),context:"file"})};t.add("autosave",function(t){var e=n(!1);return b(t),w(t,e),h(t)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("bbcode",function(){return{init:function(a){var b=this,c=a.getParam("bbcode_dialect","punbb").toLowerCase();a.on("beforeSetContent",function(a){a.content=b["_"+c+"_bbcode2html"](a.content)}),a.on("postProcess",function(a){a.set&&(a.content=b["_"+c+"_bbcode2html"](a.content)),a.get&&(a.content=b["_"+c+"_html2bbcode"](a.content))})},getInfo:function(){return{longname:"BBCode Plugin",author:"Ephox Corp",authorurl:"http://www.tinymce.com",infourl:"http://www.tinymce.com/wiki.php/Plugin:bbcode"}},_punbb_html2bbcode:function(a){function c(b,c){a=a.replace(b,c)}return a=b.trim(a),c(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),c(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),c(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),c(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),c(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),c(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),c(/<font>(.*?)<\/font>/gi,"$1"),c(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),c(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),c(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),c(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),c(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),c(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),c(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),c(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),c(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),c(/<\/(strong|b)>/gi,"[/b]"),c(/<(strong|b)>/gi,"[b]"),c(/<\/(em|i)>/gi,"[/i]"),c(/<(em|i)>/gi,"[i]"),c(/<\/u>/gi,"[/u]"),c(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),c(/<u>/gi,"[u]"),c(/<blockquote[^>]*>/gi,"[quote]"),c(/<\/blockquote>/gi,"[/quote]"),c(/<br \/>/gi,"\n"),c(/<br\/>/gi,"\n"),c(/<br>/gi,"\n"),c(/<p>/gi,""),c(/<\/p>/gi,"\n"),c(/&nbsp;|\u00a0/gi," "),c(/&quot;/gi,'"'),c(/&lt;/gi,"<"),c(/&gt;/gi,">"),c(/&amp;/gi,"&"),a},_punbb_bbcode2html:function(a){function c(b,c){a=a.replace(b,c)}return a=b.trim(a),c(/\n/gi,"<br />"),c(/\[b\]/gi,"<strong>"),c(/\[\/b\]/gi,"</strong>"),c(/\[i\]/gi,"<em>"),c(/\[\/i\]/gi,"</em>"),c(/\[u\]/gi,"<u>"),c(/\[\/u\]/gi,"</u>"),c(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),c(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),c(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),c(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),c(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span>&nbsp;'),c(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span>&nbsp;'),a}}}),function(){}}),d("0")()}();
!function(){"use strict";var o=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.util.Tools"),e=function(e){e=t.trim(e);var o=function(o,t){e=e.replace(o,t)};return o(/<a.*?href=\"(.*?)\".*?>(.*?)<\/a>/gi,"[url=$1]$2[/url]"),o(/<font.*?color=\"(.*?)\".*?class=\"codeStyle\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/<font.*?color=\"(.*?)\".*?class=\"quoteStyle\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/<font.*?class=\"codeStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[code][color=$1]$2[/color][/code]"),o(/<font.*?class=\"quoteStyle\".*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[quote][color=$1]$2[/color][/quote]"),o(/<span style=\"color: ?(.*?);\">(.*?)<\/span>/gi,"[color=$1]$2[/color]"),o(/<font.*?color=\"(.*?)\".*?>(.*?)<\/font>/gi,"[color=$1]$2[/color]"),o(/<span style=\"font-size:(.*?);\">(.*?)<\/span>/gi,"[size=$1]$2[/size]"),o(/<font>(.*?)<\/font>/gi,"$1"),o(/<img.*?src=\"(.*?)\".*?\/>/gi,"[img]$1[/img]"),o(/<span class=\"codeStyle\">(.*?)<\/span>/gi,"[code]$1[/code]"),o(/<span class=\"quoteStyle\">(.*?)<\/span>/gi,"[quote]$1[/quote]"),o(/<strong class=\"codeStyle\">(.*?)<\/strong>/gi,"[code][b]$1[/b][/code]"),o(/<strong class=\"quoteStyle\">(.*?)<\/strong>/gi,"[quote][b]$1[/b][/quote]"),o(/<em class=\"codeStyle\">(.*?)<\/em>/gi,"[code][i]$1[/i][/code]"),o(/<em class=\"quoteStyle\">(.*?)<\/em>/gi,"[quote][i]$1[/i][/quote]"),o(/<u class=\"codeStyle\">(.*?)<\/u>/gi,"[code][u]$1[/u][/code]"),o(/<u class=\"quoteStyle\">(.*?)<\/u>/gi,"[quote][u]$1[/u][/quote]"),o(/<\/(strong|b)>/gi,"[/b]"),o(/<(strong|b)>/gi,"[b]"),o(/<\/(em|i)>/gi,"[/i]"),o(/<(em|i)>/gi,"[i]"),o(/<\/u>/gi,"[/u]"),o(/<span style=\"text-decoration: ?underline;\">(.*?)<\/span>/gi,"[u]$1[/u]"),o(/<u>/gi,"[u]"),o(/<blockquote[^>]*>/gi,"[quote]"),o(/<\/blockquote>/gi,"[/quote]"),o(/<br \/>/gi,"\n"),o(/<br\/>/gi,"\n"),o(/<br>/gi,"\n"),o(/<p>/gi,""),o(/<\/p>/gi,"\n"),o(/&nbsp;|\u00a0/gi," "),o(/&quot;/gi,'"'),o(/&lt;/gi,"<"),o(/&gt;/gi,">"),o(/&amp;/gi,"&"),e},i=function(e){e=t.trim(e);var o=function(o,t){e=e.replace(o,t)};return o(/\n/gi,"<br />"),o(/\[b\]/gi,"<strong>"),o(/\[\/b\]/gi,"</strong>"),o(/\[i\]/gi,"<em>"),o(/\[\/i\]/gi,"</em>"),o(/\[u\]/gi,"<u>"),o(/\[\/u\]/gi,"</u>"),o(/\[url=([^\]]+)\](.*?)\[\/url\]/gi,'<a href="$1">$2</a>'),o(/\[url\](.*?)\[\/url\]/gi,'<a href="$1">$1</a>'),o(/\[img\](.*?)\[\/img\]/gi,'<img src="$1" />'),o(/\[color=(.*?)\](.*?)\[\/color\]/gi,'<font color="$1">$2</font>'),o(/\[code\](.*?)\[\/code\]/gi,'<span class="codeStyle">$1</span>&nbsp;'),o(/\[quote.*?\](.*?)\[\/quote\]/gi,'<span class="quoteStyle">$1</span>&nbsp;'),e};o.add("bbcode",function(){return{init:function(o){o.on("beforeSetContent",function(o){o.content=i(o.content)}),o.on("postProcess",function(o){o.set&&(o.content=i(o.content)),o.get&&(o.content=e(o.content))})}}})}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){return b.add("code",function(b){function c(){var c=b.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:b.getParam("code_dialog_width",600),minHeight:b.getParam("code_dialog_height",Math.min(a.DOM.getViewPort().h-200,500)),spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(a){b.focus(),b.undoManager.transact(function(){b.setContent(a.data.code)}),b.selection.setCursorLocation(),b.nodeChanged()}});c.find("#code").value(b.getContent({source_view:!0}))}b.addCommand("mceCodeEditor",c),b.addButton("code",{icon:"code",tooltip:"Source code",onclick:c}),b.addMenuItem("code",{icon:"code",text:"Source code",context:"tools",onclick:c})}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),o=function(t){return t.getParam("code_dialog_width",600)},i=function(t){return t.getParam("code_dialog_height",Math.min(n.DOM.getViewPort().h-200,500))},c=function(t,n){t.focus(),t.undoManager.transact(function(){t.setContent(n)}),t.selection.setCursorLocation(),t.nodeChanged()},d=function(t){return t.getContent({source_view:!0})},e=function(n){var t=o(n),e=i(n);n.windowManager.open({title:"Source code",body:{type:"textbox",name:"code",multiline:!0,minWidth:t,minHeight:e,spellcheck:!1,style:"direction: ltr; text-align: left"},onSubmit:function(t){c(n,t.data.code)}}).find("#code").value(d(n))},u=function(t){t.addCommand("mceCodeEditor",function(){e(t)})},a=function(t){t.addButton("code",{icon:"code",tooltip:"Source code",onclick:function(){e(t)}}),t.addMenuItem("code",{icon:"code",text:"Source code",onclick:function(){e(t)}})};t.add("code",function(t){return u(t),a(t),{}})}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Color")}),g("0",["1","2"],function(a,b){return a.add("colorpicker",function(a){function c(c,d){function e(a){var c=new b(a),d=c.toRgb();g.fromJSON({r:d.r,g:d.g,b:d.b,hex:c.toHex().substr(1)}),f(c.toHex())}function f(a){g.find("#preview")[0].getEl().style.background=a}var g=a.windowManager.open({title:"Color",items:{type:"container",layout:"flex",direction:"row",align:"stretch",padding:5,spacing:10,items:[{type:"colorpicker",value:d,onchange:function(){var a=this.rgb();g&&(g.find("#r").value(a.r),g.find("#g").value(a.g),g.find("#b").value(a.b),g.find("#hex").value(this.value().substr(1)),f(this.value()))}},{type:"form",padding:0,labelGap:5,defaults:{type:"textbox",size:7,value:"0",flex:1,spellcheck:!1,onchange:function(){var a,b,c=g.find("colorpicker")[0];return a=this.name(),b=this.value(),"hex"==a?(b="#"+b,e(b),void c.value(b)):(b={r:g.find("#r").value(),g:g.find("#g").value(),b:g.find("#b").value()},c.value(b),void e(b))}},items:[{name:"r",label:"R",autofocus:1},{name:"g",label:"G"},{name:"b",label:"B"},{name:"hex",label:"#",value:"000000"},{name:"preview",type:"container",border:1}]}]},onSubmit:function(){c("#"+this.toJSON().hex)}});e(d)}a.settings.color_picker_callback||(a.settings.color_picker_callback=c)}),function(){}}),d("0")()}();
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),l=tinymce.util.Tools.resolve("tinymce.util.Color"),a=function(e,n){e.find("#preview")[0].getEl().style.background=n},o=function(e,n){var i=l(n),t=i.toRgb();e.fromJSON({r:t.r,g:t.g,b:t.b,hex:i.toHex().substr(1)}),a(e,i.toHex())},t=function(e,n,i){var t=e.windowManager.open({title:"Color",items:{type:"container",layout:"flex",direction:"row",align:"stretch",padding:5,spacing:10,items:[{type:"colorpicker",value:i,onchange:function(){var e=this.rgb();t&&(t.find("#r").value(e.r),t.find("#g").value(e.g),t.find("#b").value(e.b),t.find("#hex").value(this.value().substr(1)),a(t,this.value()))}},{type:"form",padding:0,labelGap:5,defaults:{type:"textbox",size:7,value:"0",flex:1,spellcheck:!1,onchange:function(){var e,n,i=t.find("colorpicker")[0];if(e=this.name(),n=this.value(),"hex"===e)return o(t,n="#"+n),void i.value(n);n={r:t.find("#r").value(),g:t.find("#g").value(),b:t.find("#b").value()},i.value(n),o(t,n)}},items:[{name:"r",label:"R",autofocus:1},{name:"g",label:"G"},{name:"b",label:"B"},{name:"hex",label:"#",value:"000000"},{name:"preview",type:"container",border:1}]}]},onSubmit:function(){n("#"+t.toJSON().hex)}});o(t,i)};e.add("colorpicker",function(i){i.settings.color_picker_callback||(i.settings.color_picker_callback=function(e,n){t(i,e,n)})})}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("directionality",function(a){function c(c){var d,e=a.dom,f=a.selection.getSelectedBlocks();f.length&&(d=e.getAttrib(f[0],"dir"),b.each(f,function(a){e.getParent(a.parentNode,"*[dir='"+c+"']",e.getRoot())||(d!=c?e.setAttrib(a,"dir",c):e.setAttrib(a,"dir",null))}),a.nodeChanged())}function d(a){var c=[];return b.each("h1 h2 h3 h4 h5 h6 div p".split(" "),function(b){c.push(b+"[dir="+a+"]")}),c.join(",")}a.addCommand("mceDirectionLTR",function(){c("ltr")}),a.addCommand("mceDirectionRTL",function(){c("rtl")}),a.addButton("ltr",{title:"Left to right",cmd:"mceDirectionLTR",stateSelector:d("ltr")}),a.addButton("rtl",{title:"Right to left",cmd:"mceDirectionRTL",stateSelector:d("rtl")})}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),e=function(t,e){var i,n=t.dom,o=t.selection.getSelectedBlocks();o.length&&(i=n.getAttrib(o[0],"dir"),c.each(o,function(t){n.getParent(t.parentNode,'*[dir="'+e+'"]',n.getRoot())||n.setAttrib(t,"dir",i!==e?e:null)}),t.nodeChanged())},i=function(t){t.addCommand("mceDirectionLTR",function(){e(t,"ltr")}),t.addCommand("mceDirectionRTL",function(){e(t,"rtl")})},n=function(e){var i=[];return c.each("h1 h2 h3 h4 h5 h6 div p".split(" "),function(t){i.push(t+"[dir="+e+"]")}),i.join(",")},o=function(t){t.addButton("ltr",{title:"Left to right",cmd:"mceDirectionLTR",stateSelector:n("ltr")}),t.addButton("rtl",{title:"Right to left",cmd:"mceDirectionRTL",stateSelector:n("rtl")})};t.add("directionality",function(t){i(t),o(t)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("emoticons",function(a,c){function d(){var a;return a='<table role="list" class="mce-grid">',b.each(e,function(d){a+="<tr>",b.each(d,function(b){var d=c+"/img/smiley-"+b+".gif";a+='<td><a href="#" data-mce-url="'+d+'" data-mce-alt="'+b+'" tabindex="-1" role="option" aria-label="'+b+'"><img src="'+d+'" style="width: 18px; height: 18px" role="presentation" /></a></td>'}),a+="</tr>"}),a+="</table>"}var e=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]];a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:d,onclick:function(b){var c=a.dom.getParent(b.target,"a");c&&(a.insertContent('<img src="'+c.getAttribute("data-mce-url")+'" alt="'+c.getAttribute("data-mce-alt")+'" />'),this.hide())}},tooltip:"Emoticons"})}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),e=tinymce.util.Tools.resolve("tinymce.util.Tools"),n=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]],i=function(i){var o;return o='<table role="list" class="mce-grid">',e.each(n,function(t){o+="<tr>",e.each(t,function(t){var e=i+"/img/smiley-"+t+".gif";o+='<td><a href="#" data-mce-url="'+e+'" data-mce-alt="'+t+'" tabindex="-1" role="option" aria-label="'+t+'"><img src="'+e+'" style="width: 18px; height: 18px" role="presentation" /></a></td>'}),o+="</tr>"}),o+="</table>"},o=function(a,t){var e=i(t);a.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:e,onclick:function(t){var e,i,o,n=a.dom.getParent(t.target,"a");n&&(e=a,i=n.getAttribute("data-mce-url"),o=n.getAttribute("data-mce-alt"),e.insertContent(e.dom.createHTML("img",{src:i,alt:o})),this.hide())}},tooltip:"Emoticons"})};t.add("emoticons",function(t,e){o(t,e)})}();

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.dom.DOMUtils")}),g("2",["3"],function(a){return a("tinymce.PluginManager")}),g("0",["1","2"],function(a,b){var c=a.DOM;return b.add("fullscreen",function(a){function b(){var a,b,c=window,d=document,e=d.body;return e.offsetWidth&&(a=e.offsetWidth,b=e.offsetHeight),c.innerWidth&&c.innerHeight&&(a=c.innerWidth,b=c.innerHeight),{w:a,h:b}}function d(){var a=c.getViewPort();return{x:a.x,y:a.y}}function e(a){window.scrollTo(a.x,a.y)}function f(){function f(){c.setStyle(p,"height",b().h-(o.clientHeight-p.clientHeight))}var n,o,p,q,r=document.body,s=document.documentElement;m=!m,o=a.getContainer(),n=o.style,p=a.getContentAreaContainer().firstChild,q=p.style,m?(l=d(),g=q.width,h=q.height,q.width=q.height="100%",j=n.width,k=n.height,n.width=n.height="",c.addClass(r,"mce-fullscreen"),c.addClass(s,"mce-fullscreen"),c.addClass(o,"mce-fullscreen"),c.bind(window,"resize",f),f(),i=f):(q.width=g,q.height=h,j&&(n.width=j),k&&(n.height=k),c.removeClass(r,"mce-fullscreen"),c.removeClass(s,"mce-fullscreen"),c.removeClass(o,"mce-fullscreen"),c.unbind(window,"resize",i),e(l)),a.fire("FullscreenStateChanged",{state:m})}var g,h,i,j,k,l,m=!1;if(!a.settings.inline)return a.on("init",function(){a.addShortcut("Ctrl+Shift+F","",f)}),a.on("remove",function(){i&&c.unbind(window,"resize",i)}),a.addCommand("mceFullScreen",f),a.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,onClick:function(){f(),a.focus()},onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})},context:"view"}),a.addButton("fullscreen",{tooltip:"Fullscreen",shortcut:"Ctrl+Shift+F",onClick:f,onPostRender:function(){var b=this;a.on("FullscreenStateChanged",function(a){b.active(a.state)})}}),{isFullscreen:function(){return m}}}),function(){}}),d("0")()}();
!function(){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=function(e,n){e.fire("FullscreenStateChanged",{state:n})},g=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=document.body,u=document.documentElement,d=n.get(),a=function(){var e,n,t,i;g.setStyle(l,"height",(t=window,i=document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){g.unbind(window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),g.removeClass(s,"mce-fullscreen"),g.removeClass(u,"mce-fullscreen"),g.removeClass(r,"mce-fullscreen"),o=d.scrollPos,window.scrollTo(o.x,o.y),g.unbind(window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),m(e,!1);else{var f={scrollPos:(c=g.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",g.addClass(s,"mce-fullscreen"),g.addClass(u,"mce-fullscreen"),g.addClass(r,"mce-fullscreen"),g.bind(window,"resize",a),e.on("remove",h),a(),n.set(f),m(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("2",tinymce.util.Tools.resolve),g("1",["2"],function(a){return a("tinymce.PluginManager")}),g("0",["1"],function(a){return a.add("hr",function(a){a.addCommand("InsertHorizontalRule",function(){a.execCommand("mceInsertContent",!1,"<hr />")}),a.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),a.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})}),function(){}}),d("0")()}();
!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(n){n.addCommand("InsertHorizontalRule",function(){n.execCommand("mceInsertContent",!1,"<hr />")})},o=function(n){n.addButton("hr",{icon:"hr",tooltip:"Horizontal line",cmd:"InsertHorizontalRule"}),n.addMenuItem("hr",{icon:"hr",text:"Horizontal line",cmd:"InsertHorizontalRule",context:"insert"})};n.add("hr",function(n){t(n),o(n)})}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("6",tinymce.util.Tools.resolve),g("1",["6"],function(a){return a("tinymce.EditorManager")}),g("2",["6"],function(a){return a("tinymce.dom.DOMUtils")}),g("3",["6"],function(a){return a("tinymce.Env")}),g("4",["6"],function(a){return a("tinymce.PluginManager")}),g("5",["6"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2","3","4","5"],function(a,b,c,d,e){return d.add("importcss",function(d){function f(a){var b=c.cacheSuffix;return"string"==typeof a&&(a=a.replace("?"+b,"").replace("&"+b,"")),a}function g(b){var c=d.settings,e=c.skin!==!1&&(c.skin||"lightgray");if(e){var f=c.skin_url;return f=f?d.documentBaseURI.toAbsolute(f):a.baseURL+"/skins/"+e,b===f+"/content"+(d.inline?".inline":"")+".min.css"}return!1}function h(a){return"string"==typeof a?function(b){return b.indexOf(a)!==-1}:a instanceof RegExp?function(b){return a.test(b)}:a}function i(a,b){function c(a,d){var i,j=a.href;if(j=f(j),j&&b(j,d)&&!g(j)){r(a.imports,function(a){c(a,!0)});try{i=a.cssRules||a.rules}catch(a){}r(i,function(a){a.styleSheet?c(a.styleSheet,!0):a.selectorText&&r(a.selectorText.split(","),function(a){h.push(e.trim(a))})})}}var h=[],i={};r(d.contentCSS,function(a){i[a]=!0}),b||(b=function(a,b){return b||i[a]});try{r(a.styleSheets,function(a){c(a)})}catch(a){}return h}function j(a){var b,c=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(a);if(c){var f=c[1],g=c[2].substr(1).split(".").join(" "),h=e.makeMap("a,img");return c[1]?(b={title:a},d.schema.getTextBlockElements()[f]?b.block=f:d.schema.getBlockElements()[f]||h[f.toLowerCase()]?b.selector=f:b.inline=f):c[2]&&(b={inline:"span",title:a.substr(1),classes:g}),d.settings.importcss_merge_classes!==!1?b.classes=g:b.attributes={"class":g},b}}function k(a,b){return e.grep(a,function(a){return!a.filter||a.filter(b)})}function l(a){return e.map(a,function(a){return e.extend({},a,{original:a,selectors:{},filter:h(a.filter),item:{text:a.title,menu:[]}})})}function m(a,b){return null===b||a.settings.importcss_exclusive!==!1}function n(a,b,c){return!(m(d,b)?a in c:a in b.selectors)}function o(a,b,c){m(d,b)?c[a]=!0:b.selectors[a]=!0}function p(a,b,c){var e,f=d.settings;return e=c&&c.selector_converter?c.selector_converter:f.importcss_selector_converter?f.importcss_selector_converter:j,e.call(a,b,c)}var q=this,r=e.each;d.on("renderFormatsMenu",function(a){var c=d.settings,f={},g=h(c.importcss_selector_filter),j=a.control,m=l(c.importcss_groups),s=function(a,c){if(n(a,c,f)){o(a,c,f);var g=p(q,a,c);if(g){var h=g.name||b.DOM.uniqueId();return d.formatter.register(h,g),e.extend({},j.settings.itemDefaults,{text:g.title,format:h})}}return null};d.settings.importcss_append||j.items().remove(),r(i(a.doc||d.getDoc(),h(c.importcss_file_filter)),function(a){if(a.indexOf(".mce-")===-1&&(!g||g(a))){var b=k(m,a);if(b.length>0)e.each(b,function(b){var c=s(a,b);c&&b.item.menu.push(c)});else{var c=s(a,null);c&&j.add(c)}}}),r(m,function(a){a.item.menu.length>0&&j.add(a.item)}),a.control.renderNew()}),q.convertSelectorToFormat=j}),function(){}}),d("0")()}();
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),v=tinymce.util.Tools.resolve("tinymce.EditorManager"),h=tinymce.util.Tools.resolve("tinymce.Env"),y=tinymce.util.Tools.resolve("tinymce.util.Tools"),o=function(e){return e.getParam("importcss_merge_classes")},n=function(e){return e.getParam("importcss_exclusive")},_=function(e){return e.getParam("importcss_selector_converter")},r=function(e){return e.getParam("importcss_selector_filter")},i=function(e){return e.getParam("importcss_groups")},u=function(e){return e.getParam("importcss_append")},l=function(e){return e.getParam("importcss_file_filter")},a=function(t){return"string"==typeof t?function(e){return-1!==e.indexOf(t)}:t instanceof RegExp?function(e){return t.test(e)}:t},f=function(f,e,m){var g=[],n={};function p(e,t){var n,r,i,c=e.href;if(r=c,i=h.cacheSuffix,"string"==typeof r&&(r=r.replace("?"+i,"").replace("&"+i,"")),(c=r)&&m(c,t)&&(o=c,u=(s=f).settings,!(l=!1!==u.skin&&(u.skin||"lightgray"))||o!==(u.skin_url?s.documentBaseURI.toAbsolute(u.skin_url):v.baseURL+"/skins/"+l)+"/content"+(s.inline?".inline":"")+".min.css")){var s,o,u,l;y.each(e.imports,function(e){p(e,!0)});try{n=e.cssRules||e.rules}catch(a){}y.each(n,function(e){e.styleSheet?p(e.styleSheet,!0):e.selectorText&&y.each(e.selectorText.split(","),function(e){g.push(y.trim(e))})})}}y.each(f.contentCSS,function(e){n[e]=!0}),m||(m=function(e,t){return t||n[e]});try{y.each(e.styleSheets,function(e){p(e)})}catch(t){}return g},x=function(e,t){var n,r=/^(?:([a-z0-9\-_]+))?(\.[a-z0-9_\-\.]+)$/i.exec(t);if(r){var i=r[1],c=r[2].substr(1).split(".").join(" "),s=y.makeMap("a,img");return r[1]?(n={title:t},e.schema.getTextBlockElements()[i]?n.block=i:e.schema.getBlockElements()[i]||s[i.toLowerCase()]?n.selector=i:n.inline=i):r[2]&&(n={inline:"span",title:t.substr(1),classes:c}),!1!==o(e)?n.classes=c:n.attributes={"class":c},n}},T=function(e,t){return null===t||!1!==n(e)},c=x,t=function(h){h.on("renderFormatsMenu",function(e){var t,p={},c=a(r(h)),v=e.control,s=(t=i(h),y.map(t,function(e){return y.extend({},e,{original:e,selectors:{},filter:a(e.filter),item:{text:e.title,menu:[]}})})),o=function(e,t){if(f=e,g=p,!(T(h,m=t)?f in g:f in m.selectors)){u=e,a=p,T(h,l=t)?a[u]=!0:l.selectors[u]=!0;var n=(c=(i=h).plugins.importcss,s=e,((o=t)&&o.selector_converter?o.selector_converter:_(i)?_(i):function(){return x(i,s)}).call(c,s,o));if(n){var r=n.name||d.DOM.uniqueId();return h.formatter.register(r,n),y.extend({},v.settings.itemDefaults,{text:n.title,format:r})}}var i,c,s,o,u,l,a,f,m,g;return null};u(h)||v.items().remove(),y.each(f(h,e.doc||h.getDoc(),a(l(h))),function(n){if(-1===n.indexOf(".mce-")&&(!c||c(n))){var e=(r=s,i=n,y.grep(r,function(e){return!e.filter||e.filter(i)}));if(0<e.length)y.each(e,function(e){var t=o(n,e);t&&e.item.menu.push(t)});else{var t=o(n,null);t&&v.add(t)}}var r,i}),y.each(s,function(e){0<e.item.menu.length&&v.add(e.item)}),e.control.renderNew()})},s=function(t){return{convertSelectorToFormat:function(e){return c(t,e)}}};e.add("importcss",function(e){return t(e),s(e)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("insertdatetime",function(a){function c(b,c){function d(a,b){if(a=""+a,a.length<b)for(var c=0;c<b-a.length;c++)a="0"+a;return a}return c=c||new Date,b=b.replace("%D","%m/%d/%Y"),b=b.replace("%r","%I:%M:%S %p"),b=b.replace("%Y",""+c.getFullYear()),b=b.replace("%y",""+c.getYear()),b=b.replace("%m",d(c.getMonth()+1,2)),b=b.replace("%d",d(c.getDate(),2)),b=b.replace("%H",""+d(c.getHours(),2)),b=b.replace("%M",""+d(c.getMinutes(),2)),b=b.replace("%S",""+d(c.getSeconds(),2)),b=b.replace("%I",""+((c.getHours()+11)%12+1)),b=b.replace("%p",""+(c.getHours()<12?"AM":"PM")),b=b.replace("%B",""+a.translate(j[c.getMonth()])),b=b.replace("%b",""+a.translate(i[c.getMonth()])),b=b.replace("%A",""+a.translate(h[c.getDay()])),b=b.replace("%a",""+a.translate(g[c.getDay()])),b=b.replace("%%","%")}function d(b){var d=c(b);if(a.settings.insertdatetime_element){var e;e=c(/%[HMSIp]/.test(b)?"%Y-%m-%dT%H:%M":"%Y-%m-%d"),d='<time datetime="'+e+'">'+d+"</time>";var f=a.dom.getParent(a.selection.getStart(),"time");if(f)return void a.dom.setOuterHTML(f,d)}a.insertContent(d)}var e,f,g="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),h="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),i="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),j="January February March April May June July August September October November December".split(" "),k=[];a.addCommand("mceInsertDate",function(){d(a.getParam("insertdatetime_dateformat",a.translate("%Y-%m-%d")))}),a.addCommand("mceInsertTime",function(){d(a.getParam("insertdatetime_timeformat",a.translate("%H:%M:%S")))}),a.addButton("insertdatetime",{type:"splitbutton",title:"Insert date/time",onclick:function(){d(e||f)},menu:k}),b.each(a.settings.insertdatetime_formats||["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"],function(a){f||(f=a),k.push({text:c(a),onclick:function(){e=a,d(a)}})}),a.addMenuItem("insertdatetime",{icon:"date",text:"Date/time",menu:k,context:"insert"})}),function(){}}),d("0")()}();
!function(){"use strict";var r=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return r(n())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),n=function(e){return e.getParam("insertdatetime_timeformat",e.translate("%H:%M:%S"))},a=function(e){return e.getParam("insertdatetime_formats",["%H:%M:%S","%Y-%m-%d","%I:%M:%S %p","%D"])},t=function(e){return e.getParam("insertdatetime_dateformat",e.translate("%Y-%m-%d"))},i=n,o=a,u=function(e){var t=a(e);return 0<t.length?t[0]:n(e)},m=function(e){return e.getParam("insertdatetime_element",!1)},c="Sun Mon Tue Wed Thu Fri Sat Sun".split(" "),l="Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday".split(" "),s="Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),d="January February March April May June July August September October November December".split(" "),p=function(e,t){if((e=""+e).length<t)for(var n=0;n<t-e.length;n++)e="0"+e;return e},f=function(e,t,n){return n=n||new Date,t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("%D","%m/%d/%Y")).replace("%r","%I:%M:%S %p")).replace("%Y",""+n.getFullYear())).replace("%y",""+n.getYear())).replace("%m",p(n.getMonth()+1,2))).replace("%d",p(n.getDate(),2))).replace("%H",""+p(n.getHours(),2))).replace("%M",""+p(n.getMinutes(),2))).replace("%S",""+p(n.getSeconds(),2))).replace("%I",""+((n.getHours()+11)%12+1))).replace("%p",n.getHours()<12?"AM":"PM")).replace("%B",""+e.translate(d[n.getMonth()]))).replace("%b",""+e.translate(s[n.getMonth()]))).replace("%A",""+e.translate(l[n.getDay()]))).replace("%a",""+e.translate(c[n.getDay()]))).replace("%%","%")},g=function(e,t){if(m(e)){var n=f(e,t),r=void 0;r=/%[HMSIp]/.test(t)?f(e,"%Y-%m-%dT%H:%M"):f(e,"%Y-%m-%d");var a=e.dom.getParent(e.selection.getStart(),"time");a?(o=a,u=r,c=n,l=(i=e).dom.create("time",{datetime:u},c),o.parentNode.insertBefore(l,o),i.dom.remove(o),i.selection.select(l,!0),i.selection.collapse(!1)):e.insertContent('<time datetime="'+r+'">'+n+"</time>")}else e.insertContent(f(e,t));var i,o,u,c,l},y=f,M=function(e){e.addCommand("mceInsertDate",function(){g(e,t(e))}),e.addCommand("mceInsertTime",function(){g(e,i(e))})},v=tinymce.util.Tools.resolve("tinymce.util.Tools"),S=function(t,n){var r,a,e,i=(a=n,e=o(r=t),v.map(e,function(e){return{text:y(r,e),onclick:function(){a.set(e),g(r,e)}}}));t.addButton("insertdatetime",{type:"splitbutton",title:"Insert date/time",menu:i,onclick:function(){var e=n.get();g(t,e||u(t))}}),t.addMenuItem("insertdatetime",{icon:"date",text:"Date/time",menu:i,context:"insert"})};e.add("insertdatetime",function(e){var t=r(null);M(e),S(e,t)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("legacyoutput",function(a,c,d){a.settings.inline_styles=!1,a.on("init",function(){var c="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img",d=b.explode(a.settings.font_size_style_values),e=a.schema;a.formatter.register({alignleft:{selector:c,attributes:{align:"left"}},aligncenter:{selector:c,attributes:{align:"center"}},alignright:{selector:c,attributes:{align:"right"}},alignjustify:{selector:c,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all"},{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all"},{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all"},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all"},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",attributes:{face:"%value"}},fontsize:{inline:"font",attributes:{size:function(a){return b.inArray(d,a.value)+1}}},forecolor:{inline:"font",attributes:{color:"%value"}},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"}}}),b.each("b,i,u,strike".split(","),function(a){e.addValidElements(a+"[*]")}),e.getElementRule("font")||e.addValidElements("font[face|size|color|style]"),b.each(c.split(","),function(a){var b=e.getElementRule(a);b&&(b.attributes.align||(b.attributes.align={},b.attributesOrder.push("align")))})}),a.addButton("fontsizeselect",function(){var b=[],c="8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7",d=a.settings.fontsizeFormats||c;return a.$.each(d.split(" "),function(a,c){var d=c,e=c,f=c.split("=");f.length>1&&(d=f[0],e=f[1]),b.push({text:d,value:e})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:b,fixedWidth:!0,onPostRender:function(){var b=this;a.on("NodeChange",function(){var c;c=a.dom.getParent(a.selection.getNode(),"font"),c?b.value(c.size):b.value("")})},onclick:function(b){b.control.settings.value&&a.execCommand("FontSize",!1,b.control.settings.value)}}}),a.addButton("fontselect",function(){function b(a){a=a.replace(/;$/,"").split(";");for(var b=a.length;b--;)a[b]=a[b].split("=");return a}var c="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",e=[],f=b(a.settings.font_formats||c);return d.each(f,function(a,b){e.push({text:{raw:b[0]},value:b[1],textStyle:b[1].indexOf("dings")==-1?"font-family:"+b[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:function(){var b=this;a.on("NodeChange",function(){var c;c=a.dom.getParent(a.selection.getNode(),"font"),c?b.value(c.face):b.value("")})},onselect:function(b){b.control.settings.value&&a.execCommand("FontName",!1,b.control.settings.value)}}})}),function(){}}),d("0")()}();
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),t=function(a){a.settings.inline_styles=!1,a.on("init",function(){var e,t,n,i;e=a,t="p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img",n=o.explode(e.settings.font_size_style_values),i=e.schema,e.formatter.register({alignleft:{selector:t,attributes:{align:"left"}},aligncenter:{selector:t,attributes:{align:"center"}},alignright:{selector:t,attributes:{align:"right"}},alignjustify:{selector:t,attributes:{align:"justify"}},bold:[{inline:"b",remove:"all"},{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}}],italic:[{inline:"i",remove:"all"},{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}}],underline:[{inline:"u",remove:"all"},{inline:"span",styles:{textDecoration:"underline"},exact:!0}],strikethrough:[{inline:"strike",remove:"all"},{inline:"span",styles:{textDecoration:"line-through"},exact:!0}],fontname:{inline:"font",attributes:{face:"%value"}},fontsize:{inline:"font",attributes:{size:function(e){return o.inArray(n,e.value)+1}}},forecolor:{inline:"font",attributes:{color:"%value"}},hilitecolor:{inline:"font",styles:{backgroundColor:"%value"}}}),o.each("b,i,u,strike".split(","),function(e){i.addValidElements(e+"[*]")}),i.getElementRule("font")||i.addValidElements("font[face|size|color|style]"),o.each(t.split(","),function(e){var t=i.getElementRule(e);t&&(t.attributes.align||(t.attributes.align={},t.attributesOrder.push("align")))})})},n=function(i){i.addButton("fontsizeselect",function(){var o=[],e=i.settings.fontsizeFormats||"8pt=1 10pt=2 12pt=3 14pt=4 18pt=5 24pt=6 36pt=7";return i.$.each(e.split(" "),function(e,t){var n=t,i=t,a=t.split("=");1<a.length&&(n=a[0],i=a[1]),o.push({text:n,value:i})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:o,fixedWidth:!0,onPostRender:function(){var t=this;i.on("NodeChange",function(){var e;(e=i.dom.getParent(i.selection.getNode(),"font"))?t.value(e.size):t.value("")})},onclick:function(e){e.control.settings.value&&i.execCommand("FontSize",!1,e.control.settings.value)}}}),i.addButton("fontselect",function(){var n=[],e=function(e){for(var t=(e=e.replace(/;$/,"").split(";")).length;t--;)e[t]=e[t].split("=");return e}(i.settings.font_formats||"Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats");return i.$.each(e,function(e,t){n.push({text:{raw:t[0]},value:t[1],textStyle:-1===t[1].indexOf("dings")?"font-family:"+t[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:n,fixedWidth:!0,onPostRender:function(){var t=this;i.on("NodeChange",function(){var e;(e=i.dom.getParent(i.selection.getNode(),"font"))?t.value(e.face):t.value("")})},onselect:function(e){e.control.settings.value&&i.execCommand("FontName",!1,e.control.settings.value)}}})};e.add("legacyoutput",function(e){t(e),n(e)})}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("2",tinymce.util.Tools.resolve),g("1",["2"],function(a){return a("tinymce.PluginManager")}),g("0",["1"],function(a){return a.add("nonbreaking",function(a){var b=a.getParam("nonbreaking_force_tab"),c=function(a,b){for(var c="",d=0;d<b;d++)c+=a;return c},d=function(b){var d=a.plugins.visualchars&&a.plugins.visualchars.state?'<span class="mce-nbsp">&nbsp;</span>':"&nbsp;";a.insertContent(c(d,b)),a.dom.setAttrib(a.dom.select("span.mce-nbsp"),"data-mce-bogus","1")};if(a.addCommand("mceNonBreaking",function(){d(1)}),a.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),a.addMenuItem("nonbreaking",{text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"}),b){var e=+b>1?+b:3;a.on("keydown",function(a){if(9==a.keyCode){if(a.shiftKey)return;a.preventDefault(),d(e)}})}}),function(){}}),d("0")()}();
!function(){"use strict";var n=tinymce.util.Tools.resolve("tinymce.PluginManager"),i=function(n,e){var t,i=(t=n).plugins.visualchars&&t.plugins.visualchars.isEnabled()?'<span class="mce-nbsp">&nbsp;</span>':"&nbsp;";n.insertContent(function(n,e){for(var t="",i=0;i<e;i++)t+=n;return t}(i,e)),n.dom.setAttrib(n.dom.select("span.mce-nbsp"),"data-mce-bogus","1")},e=function(n){n.addCommand("mceNonBreaking",function(){i(n,1)})},o=tinymce.util.Tools.resolve("tinymce.util.VK"),a=function(n){var e=n.getParam("nonbreaking_force_tab",0);return"boolean"==typeof e?!0===e?3:0:e},t=function(e){var t=a(e);0<t&&e.on("keydown",function(n){if(n.keyCode===o.TAB&&!n.isDefaultPrevented()){if(n.shiftKey)return;n.preventDefault(),n.stopImmediatePropagation(),i(e,t)}})},r=function(n){n.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),n.addMenuItem("nonbreaking",{icon:"nonbreaking",text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"})};n.add("nonbreaking",function(n){e(n),r(n),t(n)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.util.Tools")}),g("0",["1","2"],function(a,b){return a.add("noneditable",function(a){function c(a){return function(b){return(" "+b.attr("class")+" ").indexOf(a)!==-1}}function d(c){function d(b){var c=arguments,d=c[c.length-2],e=d>0?h.charAt(d-1):"";if('"'===e)return b;if(">"===e){var f=h.lastIndexOf("<",d);if(f!==-1){var g=h.substring(f,d);if(g.indexOf('contenteditable="false"')!==-1)return b}}return'<span class="'+i+'" data-mce-content="'+a.dom.encode(c[0])+'">'+a.dom.encode("string"==typeof c[1]?c[1]:c[0])+"</span>"}var e=g.length,h=c.content,i=b.trim(f);if("raw"!=c.format){for(;e--;)h=h.replace(g[e],d);c.content=h}}var e,f,g,h="contenteditable";e=" "+b.trim(a.getParam("noneditable_editable_class","mceEditable"))+" ",f=" "+b.trim(a.getParam("noneditable_noneditable_class","mceNonEditable"))+" ";var i=c(e),j=c(f);g=a.getParam("noneditable_regexp"),g&&!g.length&&(g=[g]),a.on("PreInit",function(){g&&a.on("BeforeSetContent",d),a.parser.addAttributeFilter("class",function(a){for(var b,c=a.length;c--;)b=a[c],i(b)?b.attr(h,"true"):j(b)&&b.attr(h,"false")}),a.serializer.addAttributeFilter(h,function(a){for(var b,c=a.length;c--;)b=a[c],(i(b)||j(b))&&(g&&b.attr("data-mce-content")?(b.name="#text",b.type=3,b.raw=!0,b.value=b.attr("data-mce-content")):b.attr(h,null))})})}),function(){}}),d("0")()}();
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.util.Tools"),l=function(t){return t.getParam("noneditable_noneditable_class","mceNonEditable")},u=function(t){return t.getParam("noneditable_editable_class","mceEditable")},f=function(t){var n=t.getParam("noneditable_regexp",[]);return n&&n.constructor===RegExp?[n]:n},s=function(n){return function(t){return-1!==(" "+t.attr("class")+" ").indexOf(n)}},d=function(i,o,c){return function(t){var n=arguments,e=n[n.length-2],r=0<e?o.charAt(e-1):"";if('"'===r)return t;if(">"===r){var a=o.lastIndexOf("<",e);if(-1!==a&&-1!==o.substring(a,e).indexOf('contenteditable="false"'))return t}return'<span class="'+c+'" data-mce-content="'+i.dom.encode(n[0])+'">'+i.dom.encode("string"==typeof n[1]?n[1]:n[0])+"</span>"}},n=function(n){var t,e,r="contenteditable";t=" "+c.trim(u(n))+" ",e=" "+c.trim(l(n))+" ";var a=s(t),i=s(e),o=f(n);n.on("PreInit",function(){0<o.length&&n.on("BeforeSetContent",function(t){!function(t,n,e){var r=n.length,a=e.content;if("raw"!==e.format){for(;r--;)a=a.replace(n[r],d(t,a,l(t)));e.content=a}}(n,o,t)}),n.parser.addAttributeFilter("class",function(t){for(var n,e=t.length;e--;)n=t[e],a(n)?n.attr(r,"true"):i(n)&&n.attr(r,"false")}),n.serializer.addAttributeFilter(r,function(t){for(var n,e=t.length;e--;)n=t[e],(a(n)||i(n))&&(0<o.length&&n.attr("data-mce-content")?(n.name="#text",n.type=3,n.raw=!0,n.value=n.attr("data-mce-content")):n.attr(r,null))})})};t.add("noneditable",function(t){n(t)})}();

View File

@@ -1 +1 @@
!function(){var a={},b=function(b){for(var c=a[b],e=c.deps,f=c.defn,g=e.length,h=new Array(g),i=0;i<g;++i)h[i]=d(e[i]);var j=f.apply(null,h);if(void 0===j)throw"module ["+b+"] returned undefined";c.instance=j},c=function(b,c,d){if("string"!=typeof b)throw"module id must be a string";if(void 0===c)throw"no dependencies for "+b;if(void 0===d)throw"no definition function for "+b;a[b]={deps:c,defn:d,instance:void 0}},d=function(c){var d=a[c];if(void 0===d)throw"module ["+c+"] was undefined";return void 0===d.instance&&b(c),d.instance},e=function(a,b){for(var c=a.length,e=new Array(c),f=0;f<c;++f)e.push(d(a[f]));b.apply(null,b)},f={};f.bolt={module:{api:{define:c,require:e,demand:d}}};var g=c,h=function(a,b){g(a,[],function(){return b})};h("3",tinymce.util.Tools.resolve),g("1",["3"],function(a){return a("tinymce.PluginManager")}),g("2",["3"],function(a){return a("tinymce.Env")}),g("0",["1","2"],function(a,b){return a.add("pagebreak",function(a){var c="mce-pagebreak",d=a.getParam("pagebreak_separator","<!-- pagebreak -->"),e=new RegExp(d.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(a){return"\\"+a}),"gi"),f='<img src="'+b.transparentSrc+'" class="'+c+'" data-mce-resize="false" data-mce-placeholder />';a.addCommand("mcePageBreak",function(){a.settings.pagebreak_split_block?a.insertContent("<p>"+f+"</p>"):a.insertContent(f)}),a.addButton("pagebreak",{title:"Page break",cmd:"mcePageBreak"}),a.addMenuItem("pagebreak",{text:"Page break",icon:"pagebreak",cmd:"mcePageBreak",context:"insert"}),a.on("ResolveName",function(b){"IMG"==b.target.nodeName&&a.dom.hasClass(b.target,c)&&(b.name="pagebreak")}),a.on("click",function(b){b=b.target,"IMG"===b.nodeName&&a.dom.hasClass(b,c)&&a.selection.select(b)}),a.on("BeforeSetContent",function(a){a.content=a.content.replace(e,f)}),a.on("PreInit",function(){a.serializer.addNodeFilter("img",function(b){for(var c,e,f=b.length;f--;)if(c=b[f],e=c.attr("class"),e&&e.indexOf("mce-pagebreak")!==-1){var g=c.parent;if(a.schema.getBlockElements()[g.name]&&a.settings.pagebreak_split_block){g.type=3,g.value=d,g.raw=!0,c.remove();continue}c.type=3,c.value=d,c.raw=!0}})})}),function(){}}),d("0")()}();
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),n=function(e){return e.getParam("pagebreak_separator","\x3c!-- pagebreak --\x3e")},i=function(e){return e.getParam("pagebreak_split_block",!1)},t=function(){return"mce-pagebreak"},r=function(){return'<img src="'+a.transparentSrc+'" class="mce-pagebreak" data-mce-resize="false" data-mce-placeholder />'},c=function(c){var o=n(c),a=new RegExp(o.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,function(e){return"\\"+e}),"gi");c.on("BeforeSetContent",function(e){e.content=e.content.replace(a,r())}),c.on("PreInit",function(){c.serializer.addNodeFilter("img",function(e){for(var a,n,t=e.length;t--;)if((n=(a=e[t]).attr("class"))&&-1!==n.indexOf("mce-pagebreak")){var r=a.parent;if(c.schema.getBlockElements()[r.name]&&i(c)){r.type=3,r.value=o,r.raw=!0,a.remove();continue}a.type=3,a.value=o,a.raw=!0}})})},o=r,g=t,u=function(e){e.addCommand("mcePageBreak",function(){e.settings.pagebreak_split_block?e.insertContent("<p>"+o()+"</p>"):e.insertContent(o())})},m=function(a){a.on("ResolveName",function(e){"IMG"===e.target.nodeName&&a.dom.hasClass(e.target,g())&&(e.name="pagebreak")})},s=function(e){e.addButton("pagebreak",{title:"Page break",cmd:"mcePageBreak"}),e.addMenuItem("pagebreak",{text:"Page break",icon:"pagebreak",cmd:"mcePageBreak",context:"insert"})};e.add("pagebreak",function(e){u(e),s(e),c(e),m(e)})}();

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