Dan Brown

Merge branch 'master' into release

...@@ -7,7 +7,6 @@ Homestead.yaml ...@@ -7,7 +7,6 @@ Homestead.yaml
7 /public/plugins 7 /public/plugins
8 /public/css/*.map 8 /public/css/*.map
9 /public/js/*.map 9 /public/js/*.map
10 -/public/uploads
11 /public/bower 10 /public/bower
12 /storage/images 11 /storage/images
13 _ide_helper.php 12 _ide_helper.php
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 3
4 -class ConfirmationEmailException extends NotifyException
5 -{
6 -
7 -}
...\ No newline at end of file ...\ No newline at end of file
4 +class ConfirmationEmailException extends NotifyException {}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -5,6 +5,7 @@ namespace BookStack\Exceptions; ...@@ -5,6 +5,7 @@ namespace BookStack\Exceptions;
5 use Exception; 5 use Exception;
6 use Illuminate\Contracts\Validation\ValidationException; 6 use Illuminate\Contracts\Validation\ValidationException;
7 use Illuminate\Database\Eloquent\ModelNotFoundException; 7 use Illuminate\Database\Eloquent\ModelNotFoundException;
8 +use PhpSpec\Exception\Example\ErrorException;
8 use Symfony\Component\HttpKernel\Exception\HttpException; 9 use Symfony\Component\HttpKernel\Exception\HttpException;
9 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; 10 use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
10 use Illuminate\Auth\Access\AuthorizationException; 11 use Illuminate\Auth\Access\AuthorizationException;
...@@ -38,17 +39,26 @@ class Handler extends ExceptionHandler ...@@ -38,17 +39,26 @@ class Handler extends ExceptionHandler
38 /** 39 /**
39 * Render an exception into an HTTP response. 40 * Render an exception into an HTTP response.
40 * 41 *
41 - * @param \Illuminate\Http\Request $request 42 + * @param \Illuminate\Http\Request $request
42 - * @param \Exception $e 43 + * @param \Exception $e
43 * @return \Illuminate\Http\Response 44 * @return \Illuminate\Http\Response
44 */ 45 */
45 public function render($request, Exception $e) 46 public function render($request, Exception $e)
46 { 47 {
47 - if($e instanceof NotifyException) { 48 + // Handle notify exceptions which will redirect to the
49 + // specified location then show a notification message.
50 + if ($e instanceof NotifyException) {
48 \Session::flash('error', $e->message); 51 \Session::flash('error', $e->message);
49 return response()->redirectTo($e->redirectLocation); 52 return response()->redirectTo($e->redirectLocation);
50 } 53 }
51 54
55 + // Handle pretty exceptions which will show a friendly application-fitting page
56 + // Which will include the basic message to point the user roughly to the cause.
57 + if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
58 + $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
59 + return response()->view('errors/500', ['message' => $message], 500);
60 + }
61 +
52 return parent::render($request, $e); 62 return parent::render($request, $e);
53 } 63 }
54 } 64 }
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 -
4 -use Exception;
5 -
6 -class ImageUploadException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
3 +class ImageUploadException extends PrettyException {}
...\ No newline at end of file ...\ No newline at end of file
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 -
4 -use Exception;
5 -
6 -class LdapException extends Exception
7 -{
8 -
9 -}
...\ No newline at end of file ...\ No newline at end of file
3 +class LdapException extends PrettyException {}
...\ No newline at end of file ...\ No newline at end of file
......
1 +<?php namespace BookStack\Exceptions;
2 +
3 +use Exception;
4 +
5 +class PrettyException extends Exception {}
...\ No newline at end of file ...\ No newline at end of file
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 3
4 -class SocialDriverNotConfigured extends \Exception
5 -{
6 -}
...\ No newline at end of file ...\ No newline at end of file
4 +class SocialDriverNotConfigured extends PrettyException {}
...\ No newline at end of file ...\ No newline at end of file
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 3
4 -class SocialSignInException extends NotifyException
5 -{
6 -
7 -}
...\ No newline at end of file ...\ No newline at end of file
4 +class SocialSignInException extends NotifyException {}
...\ No newline at end of file ...\ No newline at end of file
......
1 <?php namespace BookStack\Exceptions; 1 <?php namespace BookStack\Exceptions;
2 2
3 3
4 -class UserRegistrationException extends NotifyException
5 -{
6 -
7 -}
...\ No newline at end of file ...\ No newline at end of file
4 +class UserRegistrationException extends NotifyException {}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -130,8 +130,8 @@ class UserController extends Controller ...@@ -130,8 +130,8 @@ class UserController extends Controller
130 }); 130 });
131 131
132 $this->validate($request, [ 132 $this->validate($request, [
133 - 'name' => 'required', 133 + 'name' => 'min:2',
134 - 'email' => 'required|email|unique:users,email,' . $id, 134 + 'email' => 'min:2|email|unique:users,email,' . $id,
135 'password' => 'min:5|required_with:password_confirm', 135 'password' => 'min:5|required_with:password_confirm',
136 'password-confirm' => 'same:password|required_with:password', 136 'password-confirm' => 'same:password|required_with:password',
137 'role' => 'exists:roles,id' 137 'role' => 'exists:roles,id'
......
...@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException; ...@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
4 use BookStack\Image; 4 use BookStack\Image;
5 use BookStack\User; 5 use BookStack\User;
6 use Exception; 6 use Exception;
7 +use Intervention\Image\Exception\NotSupportedException;
7 use Intervention\Image\ImageManager; 8 use Intervention\Image\ImageManager;
8 use Illuminate\Contracts\Filesystem\Factory as FileSystem; 9 use Illuminate\Contracts\Filesystem\Factory as FileSystem;
9 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance; 10 use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
...@@ -119,10 +120,12 @@ class ImageService ...@@ -119,10 +120,12 @@ class ImageService
119 * Checks the cache then storage to avoid creating / accessing the filesystem on every check. 120 * Checks the cache then storage to avoid creating / accessing the filesystem on every check.
120 * 121 *
121 * @param Image $image 122 * @param Image $image
122 - * @param int $width 123 + * @param int $width
123 - * @param int $height 124 + * @param int $height
124 - * @param bool $keepRatio 125 + * @param bool $keepRatio
125 * @return string 126 * @return string
127 + * @throws Exception
128 + * @throws ImageUploadException
126 */ 129 */
127 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false) 130 public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
128 { 131 {
...@@ -139,8 +142,16 @@ class ImageService ...@@ -139,8 +142,16 @@ class ImageService
139 return $this->getPublicUrl($thumbFilePath); 142 return $this->getPublicUrl($thumbFilePath);
140 } 143 }
141 144
142 - // Otherwise create the thumbnail 145 + try {
143 - $thumb = $this->imageTool->make($storage->get($image->path)); 146 + $thumb = $this->imageTool->make($storage->get($image->path));
147 + } catch (Exception $e) {
148 + if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
149 + throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
150 + } else {
151 + throw $e;
152 + }
153 + }
154 +
144 if ($keepRatio) { 155 if ($keepRatio) {
145 $thumb->resize($width, null, function ($constraint) { 156 $thumb->resize($width, null, function ($constraint) {
146 $constraint->aspectRatio(); 157 $constraint->aspectRatio();
......
...@@ -46,7 +46,7 @@ class LdapService ...@@ -46,7 +46,7 @@ class LdapService
46 46
47 $user = $users[0]; 47 $user = $users[0];
48 return [ 48 return [
49 - 'uid' => $user['uid'][0], 49 + 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
50 'name' => $user['cn'][0], 50 'name' => $user['cn'][0],
51 'dn' => $user['dn'], 51 'dn' => $user['dn'],
52 'email' => (isset($user['mail'])) ? $user['mail'][0] : null 52 'email' => (isset($user['mail'])) ? $user['mail'][0] : null
......
...@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration ...@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
28 $table->dropColumn('external_auth_id'); 28 $table->dropColumn('external_auth_id');
29 }); 29 });
30 } 30 }
31 -} 31 +}
...\ No newline at end of file ...\ No newline at end of file
......
1 +*
2 +!.gitignore
...\ No newline at end of file ...\ No newline at end of file
...@@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS ...@@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS
17 17
18 ## Requirements 18 ## Requirements
19 19
20 -BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing. 20 +BookStack has similar requirements to Laravel:
21 21
22 * PHP >= 5.5.9, Will need to be usable from the command line. 22 * PHP >= 5.5.9, Will need to be usable from the command line.
23 -* OpenSSL PHP Extension 23 +* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
24 -* PDO PHP Extension
25 -* MBstring PHP Extension
26 -* Tokenizer PHP Extension
27 * MySQL >= 5.6 24 * MySQL >= 5.6
28 * Git (Not strictly required but helps manage updates) 25 * Git (Not strictly required but helps manage updates)
29 * [Composer](https://getcomposer.org/) 26 * [Composer](https://getcomposer.org/)
30 -* [Node.js](https://nodejs.org/en/) **Development Only**
31 -* [Gulp](http://gulpjs.com/) **Development Only**
32 -
33 27
34 ## Installation 28 ## Installation
35 29
...@@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui ...@@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui
144 138
145 You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID. 139 You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID.
146 140
147 -## Testing 141 +## Development & Testing
142 +
143 +All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
144 +
145 +* [Node.js](https://nodejs.org/en/) **Development Only**
146 +* [Gulp](http://gulpjs.com/) **Development Only**
147 +
148 +SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
148 149
149 BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing. 150 BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.
150 151
......
1 "use strict"; 1 "use strict";
2 2
3 -module.exports = function (ngApp) { 3 +module.exports = function (ngApp, events) {
4 4
5 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', 5 ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
6 function ($scope, $attrs, $http, $timeout, imageManagerService) { 6 function ($scope, $attrs, $http, $timeout, imageManagerService) {
...@@ -17,21 +17,40 @@ module.exports = function (ngApp) { ...@@ -17,21 +17,40 @@ module.exports = function (ngApp) {
17 var dataLoaded = false; 17 var dataLoaded = false;
18 var callback = false; 18 var callback = false;
19 19
20 + /**
21 + * Simple returns the appropriate upload url depending on the image type set.
22 + * @returns {string}
23 + */
20 $scope.getUploadUrl = function () { 24 $scope.getUploadUrl = function () {
21 return '/images/' + $scope.imageType + '/upload'; 25 return '/images/' + $scope.imageType + '/upload';
22 }; 26 };
23 27
28 + /**
29 + * Runs on image upload, Adds an image to local list of images
30 + * and shows a success message to the user.
31 + * @param file
32 + * @param data
33 + */
24 $scope.uploadSuccess = function (file, data) { 34 $scope.uploadSuccess = function (file, data) {
25 $scope.$apply(() => { 35 $scope.$apply(() => {
26 $scope.images.unshift(data); 36 $scope.images.unshift(data);
27 }); 37 });
38 + events.emit('success', 'Image uploaded');
28 }; 39 };
29 40
41 + /**
42 + * Runs the callback and hides the image manager.
43 + * @param returnData
44 + */
30 function callbackAndHide(returnData) { 45 function callbackAndHide(returnData) {
31 if (callback) callback(returnData); 46 if (callback) callback(returnData);
32 $scope.showing = false; 47 $scope.showing = false;
33 } 48 }
34 49
50 + /**
51 + * Image select action. Checks if a double-click was fired.
52 + * @param image
53 + */
35 $scope.imageSelect = function (image) { 54 $scope.imageSelect = function (image) {
36 var dblClickTime = 300; 55 var dblClickTime = 300;
37 var currentTime = Date.now(); 56 var currentTime = Date.now();
...@@ -48,10 +67,19 @@ module.exports = function (ngApp) { ...@@ -48,10 +67,19 @@ module.exports = function (ngApp) {
48 previousClickTime = currentTime; 67 previousClickTime = currentTime;
49 }; 68 };
50 69
70 + /**
71 + * Action that runs when the 'Select image' button is clicked.
72 + * Runs the callback and hides the image manager.
73 + */
51 $scope.selectButtonClick = function () { 74 $scope.selectButtonClick = function () {
52 callbackAndHide($scope.selectedImage); 75 callbackAndHide($scope.selectedImage);
53 }; 76 };
54 77
78 + /**
79 + * Show the image manager.
80 + * Takes a callback to execute later on.
81 + * @param doneCallback
82 + */
55 function show(doneCallback) { 83 function show(doneCallback) {
56 callback = doneCallback; 84 callback = doneCallback;
57 $scope.showing = true; 85 $scope.showing = true;
...@@ -62,6 +90,8 @@ module.exports = function (ngApp) { ...@@ -62,6 +90,8 @@ module.exports = function (ngApp) {
62 } 90 }
63 } 91 }
64 92
93 + // Connects up the image manger so it can be used externally
94 + // such as from TinyMCE.
65 imageManagerService.show = show; 95 imageManagerService.show = show;
66 imageManagerService.showExternal = function (doneCallback) { 96 imageManagerService.showExternal = function (doneCallback) {
67 $scope.$apply(() => { 97 $scope.$apply(() => {
...@@ -70,10 +100,16 @@ module.exports = function (ngApp) { ...@@ -70,10 +100,16 @@ module.exports = function (ngApp) {
70 }; 100 };
71 window.ImageManager = imageManagerService; 101 window.ImageManager = imageManagerService;
72 102
103 + /**
104 + * Hide the image manager
105 + */
73 $scope.hide = function () { 106 $scope.hide = function () {
74 $scope.showing = false; 107 $scope.showing = false;
75 }; 108 };
76 109
110 + /**
111 + * Fetch the list image data from the server.
112 + */
77 function fetchData() { 113 function fetchData() {
78 var url = '/images/' + $scope.imageType + '/all/' + page; 114 var url = '/images/' + $scope.imageType + '/all/' + page;
79 $http.get(url).then((response) => { 115 $http.get(url).then((response) => {
...@@ -82,28 +118,33 @@ module.exports = function (ngApp) { ...@@ -82,28 +118,33 @@ module.exports = function (ngApp) {
82 page++; 118 page++;
83 }); 119 });
84 } 120 }
121 + $scope.fetchData = fetchData;
85 122
123 + /**
124 + * Save the details of an image.
125 + * @param event
126 + */
86 $scope.saveImageDetails = function (event) { 127 $scope.saveImageDetails = function (event) {
87 event.preventDefault(); 128 event.preventDefault();
88 var url = '/images/update/' + $scope.selectedImage.id; 129 var url = '/images/update/' + $scope.selectedImage.id;
89 $http.put(url, this.selectedImage).then((response) => { 130 $http.put(url, this.selectedImage).then((response) => {
90 - $scope.imageUpdateSuccess = true; 131 + events.emit('success', 'Image details updated');
91 - $timeout(() => {
92 - $scope.imageUpdateSuccess = false;
93 - }, 3000);
94 }, (response) => { 132 }, (response) => {
95 var errors = response.data; 133 var errors = response.data;
96 var message = ''; 134 var message = '';
97 Object.keys(errors).forEach((key) => { 135 Object.keys(errors).forEach((key) => {
98 message += errors[key].join('\n'); 136 message += errors[key].join('\n');
99 }); 137 });
100 - $scope.imageUpdateFailure = message; 138 + events.emit('error', message);
101 - $timeout(() => {
102 - $scope.imageUpdateFailure = false;
103 - }, 5000);
104 }); 139 });
105 }; 140 };
106 141
142 + /**
143 + * Delete an image from system and notify of success.
144 + * Checks if it should force delete when an image
145 + * has dependant pages.
146 + * @param event
147 + */
107 $scope.deleteImage = function (event) { 148 $scope.deleteImage = function (event) {
108 event.preventDefault(); 149 event.preventDefault();
109 var force = $scope.dependantPages !== false; 150 var force = $scope.dependantPages !== false;
...@@ -112,10 +153,7 @@ module.exports = function (ngApp) { ...@@ -112,10 +153,7 @@ module.exports = function (ngApp) {
112 $http.delete(url).then((response) => { 153 $http.delete(url).then((response) => {
113 $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); 154 $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
114 $scope.selectedImage = false; 155 $scope.selectedImage = false;
115 - $scope.imageDeleteSuccess = true; 156 + events.emit('success', 'Image successfully deleted');
116 - $timeout(() => {
117 - $scope.imageDeleteSuccess = false;
118 - }, 3000);
119 }, (response) => { 157 }, (response) => {
120 // Pages failure 158 // Pages failure
121 if (response.status === 400) { 159 if (response.status === 400) {
...@@ -124,6 +162,15 @@ module.exports = function (ngApp) { ...@@ -124,6 +162,15 @@ module.exports = function (ngApp) {
124 }); 162 });
125 }; 163 };
126 164
165 + /**
166 + * Simple date creator used to properly format dates.
167 + * @param stringDate
168 + * @returns {Date}
169 + */
170 + $scope.getDate = function(stringDate) {
171 + return new Date(stringDate);
172 + };
173 +
127 }]); 174 }]);
128 175
129 176
......
...@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html'); ...@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
5 var imagePickerTemplate = require('./components/image-picker.html'); 5 var imagePickerTemplate = require('./components/image-picker.html');
6 var dropZoneTemplate = require('./components/drop-zone.html'); 6 var dropZoneTemplate = require('./components/drop-zone.html');
7 7
8 -module.exports = function (ngApp) { 8 +module.exports = function (ngApp, events) {
9 9
10 /** 10 /**
11 * Toggle Switches 11 * Toggle Switches
......
1 - 1 +"use strict";
2 2
3 // AngularJS - Create application and load components 3 // AngularJS - Create application and load components
4 var angular = require('angular'); 4 var angular = require('angular');
...@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate'); ...@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
7 var ngSanitize = require('angular-sanitize'); 7 var ngSanitize = require('angular-sanitize');
8 8
9 var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); 9 var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
10 -var services = require('./services')(ngApp); 10 +
11 -var directives = require('./directives')(ngApp); 11 +
12 -var controllers = require('./controllers')(ngApp); 12 +// Global Event System
13 +var Events = {
14 + listeners: {},
15 + emit: function (eventName, eventData) {
16 + if (typeof this.listeners[eventName] === 'undefined') return this;
17 + var eventsToStart = this.listeners[eventName];
18 + for (let i = 0; i < eventsToStart.length; i++) {
19 + var event = eventsToStart[i];
20 + event(eventData);
21 + }
22 + return this;
23 + },
24 + listen: function (eventName, callback) {
25 + if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
26 + this.listeners[eventName].push(callback);
27 + return this;
28 + }
29 +};
30 +window.Events = Events;
31 +
32 +var services = require('./services')(ngApp, Events);
33 +var directives = require('./directives')(ngApp, Events);
34 +var controllers = require('./controllers')(ngApp, Events);
13 35
14 //Global jQuery Config & Extensions 36 //Global jQuery Config & Extensions
15 37
...@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) { ...@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
32 // Global jQuery Elements 54 // Global jQuery Elements
33 $(function () { 55 $(function () {
34 56
57 +
58 + var notifications = $('.notification');
59 + var successNotification = notifications.filter('.pos');
60 + var errorNotification = notifications.filter('.neg');
61 + // Notification Events
62 + window.Events.listen('success', function (text) {
63 + successNotification.hide();
64 + successNotification.find('span').text(text);
65 + setTimeout(() => {
66 + successNotification.show();
67 + }, 1);
68 + });
69 + window.Events.listen('error', function (text) {
70 + errorNotification.find('span').text(text);
71 + errorNotification.show();
72 + });
73 +
35 // Notification hiding 74 // Notification hiding
36 - $('.notification').click(function () { 75 + notifications.click(function () {
37 $(this).fadeOut(100); 76 $(this).fadeOut(100);
38 }); 77 });
39 78
...@@ -44,6 +83,29 @@ $(function () { ...@@ -44,6 +83,29 @@ $(function () {
44 $(this).closest('.chapter').find('.inset-list').slideToggle(180); 83 $(this).closest('.chapter').find('.inset-list').slideToggle(180);
45 }); 84 });
46 85
86 + // Back to top button
87 + $('#back-to-top').click(function() {
88 + $('#header').smoothScrollTo();
89 + });
90 + var scrollTopShowing = false;
91 + var scrollTop = document.getElementById('back-to-top');
92 + var scrollTopBreakpoint = 1200;
93 + window.addEventListener('scroll', function() {
94 + if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
95 + scrollTop.style.display = 'block';
96 + scrollTopShowing = true;
97 + setTimeout(() => {
98 + scrollTop.style.opacity = 1;
99 + }, 1);
100 + } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
101 + scrollTop.style.opacity = 0;
102 + scrollTopShowing = false;
103 + setTimeout(() => {
104 + scrollTop.style.display = 'none';
105 + }, 500);
106 + }
107 + });
108 +
47 }); 109 });
48 110
49 111
......
...@@ -8,7 +8,6 @@ module.exports = { ...@@ -8,7 +8,6 @@ module.exports = {
8 statusbar: false, 8 statusbar: false,
9 menubar: false, 9 menubar: false,
10 paste_data_images: false, 10 paste_data_images: false,
11 - //height: 700,
12 extended_valid_elements: 'pre[*]', 11 extended_valid_elements: 'pre[*]',
13 automatic_uploads: false, 12 automatic_uploads: false,
14 valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]", 13 valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
...@@ -31,7 +30,7 @@ module.exports = { ...@@ -31,7 +30,7 @@ module.exports = {
31 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, 30 alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
32 }, 31 },
33 file_browser_callback: function (field_name, url, type, win) { 32 file_browser_callback: function (field_name, url, type, win) {
34 - ImageManager.show(function (image) { 33 + window.ImageManager.showExternal(function (image) {
35 win.document.getElementById(field_name).value = image.url; 34 win.document.getElementById(field_name).value = image.url;
36 if ("createEvent" in document) { 35 if ("createEvent" in document) {
37 var evt = document.createEvent("HTMLEvents"); 36 var evt = document.createEvent("HTMLEvents");
...@@ -40,6 +39,10 @@ module.exports = { ...@@ -40,6 +39,10 @@ module.exports = {
40 } else { 39 } else {
41 win.document.getElementById(field_name).fireEvent("onchange"); 40 win.document.getElementById(field_name).fireEvent("onchange");
42 } 41 }
42 + var html = '<a href="' + image.url + '" target="_blank">';
43 + html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
44 + html += '</a>';
45 + win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
43 }); 46 });
44 }, 47 },
45 paste_preprocess: function (plugin, args) { 48 paste_preprocess: function (plugin, args) {
......
1 "use strict"; 1 "use strict";
2 2
3 -module.exports = function(ngApp) { 3 +module.exports = function(ngApp, events) {
4 4
5 ngApp.factory('imageManagerService', function() { 5 ngApp.factory('imageManagerService', function() {
6 return { 6 return {
......
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
21 border-radius: 4px; 21 border-radius: 4px;
22 box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); 22 box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
23 overflow: hidden; 23 overflow: hidden;
24 - max-width: 1340px;
25 position: fixed; 24 position: fixed;
26 top: 0; 25 top: 0;
27 bottom: 0; 26 bottom: 0;
...@@ -44,18 +43,49 @@ ...@@ -44,18 +43,49 @@
44 right: 0; 43 right: 0;
45 } 44 }
46 45
47 -.image-manager-list img { 46 +.image-manager-list .image {
48 display: block; 47 display: block;
48 + position: relative;
49 border-radius: 0; 49 border-radius: 0;
50 float: left; 50 float: left;
51 margin: 0; 51 margin: 0;
52 cursor: pointer; 52 cursor: pointer;
53 width: (100%/6); 53 width: (100%/6);
54 height: auto; 54 height: auto;
55 - border: 1px solid #FFF; 55 + border: 1px solid #DDD;
56 + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
56 transition: all cubic-bezier(.4, 0, 1, 1) 160ms; 57 transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
58 + overflow: hidden;
57 &.selected { 59 &.selected {
58 transform: scale3d(0.92, 0.92, 0.92); 60 transform: scale3d(0.92, 0.92, 0.92);
61 + border: 1px solid #444;
62 + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
63 + }
64 + img {
65 + width: 100%;
66 + max-width: 100%;
67 + display: block;
68 + }
69 + .image-meta {
70 + position: absolute;
71 + width: 100%;
72 + bottom: 0;
73 + left: 0;
74 + color: #EEE;
75 + background-color: rgba(0, 0, 0, 0.4);
76 + font-size: 10px;
77 + padding: 3px 4px;
78 + span {
79 + display: block;
80 + }
81 + }
82 + @include smaller-than($xl) {
83 + width: (100%/4);
84 + }
85 + @include smaller-than($m) {
86 + .image-meta {
87 + display: none;
88 + }
59 } 89 }
60 } 90 }
61 91
......
...@@ -105,8 +105,8 @@ ...@@ -105,8 +105,8 @@
105 } 105 }
106 .book-tree .sidebar-page-list { 106 .book-tree .sidebar-page-list {
107 list-style: none; 107 list-style: none;
108 - margin: 0; 108 + margin: $-xs 0 0;
109 - margin-top: $-xs; 109 + padding-left: 0;
110 border-left: 5px solid $color-book; 110 border-left: 5px solid $color-book;
111 li a { 111 li a {
112 display: block; 112 display: block;
......
...@@ -223,13 +223,13 @@ span.highlight { ...@@ -223,13 +223,13 @@ span.highlight {
223 * Lists 223 * Lists
224 */ 224 */
225 ul { 225 ul {
226 - list-style: disc; 226 + padding-left: $-m * 1.5;
227 - margin-left: $-m*1.5; 227 + list-style: disc inside;
228 } 228 }
229 229
230 ol { 230 ol {
231 - list-style: decimal; 231 + list-style: decimal inside;
232 - margin-left: $-m*1.5; 232 + padding-left: $-m * 1.5;
233 } 233 }
234 234
235 /* 235 /*
......
...@@ -9,4 +9,9 @@ ...@@ -9,4 +9,9 @@
9 @import "tables"; 9 @import "tables";
10 @import "header"; 10 @import "header";
11 @import "lists"; 11 @import "lists";
12 -@import "pages";
...\ No newline at end of file ...\ No newline at end of file
12 +@import "pages";
13 +
14 +table {
15 + border-spacing: 0;
16 + border-collapse: collapse;
17 +}
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -126,4 +126,42 @@ $loadingSize: 10px; ...@@ -126,4 +126,42 @@ $loadingSize: 10px;
126 i { 126 i {
127 padding-right: $-s; 127 padding-right: $-s;
128 } 128 }
129 +}
130 +
131 +// Back to top link
132 +$btt-size: 40px;
133 +#back-to-top {
134 + background-color: rgba($primary, 0.4);
135 + position: fixed;
136 + bottom: $-m;
137 + right: $-m;
138 + padding: $-xs $-s;
139 + cursor: pointer;
140 + color: #FFF;
141 + width: $btt-size;
142 + height: $btt-size;
143 + border-radius: $btt-size;
144 + transition: all ease-in-out 180ms;
145 + opacity: 0;
146 + z-index: 999;
147 + &:hover {
148 + width: $btt-size*3.4;
149 + background-color: rgba($primary, 1);
150 + span {
151 + display: inline-block;
152 + }
153 + }
154 + .inner {
155 + width: $btt-size*3.4;
156 + }
157 + i {
158 + margin: 0;
159 + font-size: 28px;
160 + padding: 0 $-s 0 0;
161 + }
162 + span {
163 + line-height: 12px;
164 + position: relative;
165 + top: -5px;
166 + }
129 } 167 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -77,6 +77,11 @@ ...@@ -77,6 +77,11 @@
77 @yield('content') 77 @yield('content')
78 </section> 78 </section>
79 79
80 + <div id="back-to-top">
81 + <div class="inner">
82 + <i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
83 + </div>
84 + </div>
80 @yield('bottom') 85 @yield('bottom')
81 <script src="{{ versioned_asset('js/common.js') }}"></script> 86 <script src="{{ versioned_asset('js/common.js') }}"></script>
82 @yield('scripts') 87 @yield('scripts')
......
1 +@extends('base')
2 +
3 +@section('content')
4 +
5 + <div class="container">
6 + <h1 class="text-muted">An Error Occurred</h1>
7 + <p>{{ $message }}</p>
8 + </div>
9 +
10 +@stop
...\ No newline at end of file ...\ No newline at end of file
...@@ -7,12 +7,12 @@ ...@@ -7,12 +7,12 @@
7 <div class="faded-small"> 7 <div class="faded-small">
8 <div class="container"> 8 <div class="container">
9 <div class="row"> 9 <div class="row">
10 - <div class="col-md-4 faded"> 10 + <div class="col-sm-4 faded">
11 <div class="action-buttons text-left"> 11 <div class="action-buttons text-left">
12 <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a> 12 <a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
13 </div> 13 </div>
14 </div> 14 </div>
15 - <div class="col-md-8 faded"> 15 + <div class="col-sm-8 faded">
16 <div class="action-buttons"> 16 <div class="action-buttons">
17 <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a> 17 <a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a>
18 <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button> 18 <button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>
......
...@@ -20,5 +20,11 @@ ...@@ -20,5 +20,11 @@
20 table td { 20 table td {
21 width: auto !important; 21 width: auto !important;
22 } 22 }
23 +
24 + .page-content img.align-left, .page-content img.align-right {
25 + float: none !important;
26 + clear: both;
27 + display: block;
28 + }
23 </style> 29 </style>
24 @stop 30 @stop
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
22 <span dropdown class="dropdown-container"> 22 <span dropdown class="dropdown-container">
23 <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div> 23 <div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
24 <ul class="wide"> 24 <ul class="wide">
25 - <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li> 25 + <li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
26 - <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li> 26 + <li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
27 - <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li> 27 + <li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
28 </ul> 28 </ul>
29 </span> 29 </span>
30 @if($currentUser->can('page-update')) 30 @if($currentUser->can('page-update'))
......
...@@ -5,11 +5,14 @@ ...@@ -5,11 +5,14 @@
5 <div class="image-manager-content"> 5 <div class="image-manager-content">
6 <div class="image-manager-list"> 6 <div class="image-manager-list">
7 <div ng-repeat="image in images"> 7 <div ng-repeat="image in images">
8 - <img class="anim fadeIn" 8 + <div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
9 - ng-class="{selected: (image==selectedImage)}" 9 + ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
10 - ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}" 10 + <img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
11 - ng-click="imageSelect(image)" 11 + <div class="image-meta">
12 - ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"> 12 + <span class="name" ng-bind="image.name"></span>
13 + <span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
14 + </div>
15 + </div>
13 </div> 16 </div>
14 <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div> 17 <div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
15 </div> 18 </div>
...@@ -19,18 +22,20 @@ ...@@ -19,18 +22,20 @@
19 22
20 <div class="image-manager-sidebar"> 23 <div class="image-manager-sidebar">
21 <h2>Images</h2> 24 <h2>Images</h2>
22 - <hr class="even">
23 <drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone> 25 <drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone>
24 <div class="image-manager-details anim fadeIn" ng-show="selectedImage"> 26 <div class="image-manager-details anim fadeIn" ng-show="selectedImage">
25 27
26 <hr class="even"> 28 <hr class="even">
27 29
28 <form ng-submit="saveImageDetails($event)"> 30 <form ng-submit="saveImageDetails($event)">
31 + <div>
32 + <a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
33 + <img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
34 + </a>
35 + </div>
29 <div class="form-group"> 36 <div class="form-group">
30 <label for="name">Image Name</label> 37 <label for="name">Image Name</label>
31 <input type="text" id="name" name="name" ng-model="selectedImage.name"> 38 <input type="text" id="name" name="name" ng-model="selectedImage.name">
32 - <p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p>
33 - <p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p>
34 </div> 39 </div>
35 </form> 40 </form>
36 41
...@@ -53,8 +58,6 @@ ...@@ -53,8 +58,6 @@
53 </form> 58 </form>
54 </div> 59 </div>
55 60
56 - <p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p>
57 -
58 <div class="image-manager-bottom"> 61 <div class="image-manager-bottom">
59 <button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()"> 62 <button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
60 <i class="zmdi zmdi-square-right"></i>Select Image 63 <i class="zmdi zmdi-square-right"></i>Select Image
......
1 -@if(Session::has('success'))
2 - <div class="notification anim pos">
3 - <i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span>
4 - </div>
5 -@endif
6 1
7 -@if(Session::has('error'))
8 - <div class="notification anim neg stopped">
9 - <i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
10 - </div>
11 -@endif
...\ No newline at end of file ...\ No newline at end of file
2 +<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
3 + <i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span>
4 +</div>
5 +
6 +<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
7 + <i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
8 +</div>
......
...@@ -28,7 +28,7 @@ class LdapTest extends \TestCase ...@@ -28,7 +28,7 @@ class LdapTest extends \TestCase
28 ->andReturn(['count' => 1, 0 => [ 28 ->andReturn(['count' => 1, 0 => [
29 'uid' => [$this->mockUser->name], 29 'uid' => [$this->mockUser->name],
30 'cn' => [$this->mockUser->name], 30 'cn' => [$this->mockUser->name],
31 - 'dn' => ['dc=test'.config('services.ldap.base_dn')] 31 + 'dn' => ['dc=test' . config('services.ldap.base_dn')]
32 ]]); 32 ]]);
33 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true); 33 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
34 34
...@@ -46,6 +46,30 @@ class LdapTest extends \TestCase ...@@ -46,6 +46,30 @@ class LdapTest extends \TestCase
46 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]); 46 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
47 } 47 }
48 48
49 + public function test_login_works_when_no_uid_provided_by_ldap_server()
50 + {
51 + $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
52 + $this->mockLdap->shouldReceive('setOption')->once();
53 + $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
54 + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
55 + ->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
56 + ->andReturn(['count' => 1, 0 => [
57 + 'cn' => [$this->mockUser->name],
58 + 'dn' => $ldapDn,
59 + 'mail' => [$this->mockUser->email]
60 + ]]);
61 + $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
62 +
63 + $this->visit('/login')
64 + ->see('Username')
65 + ->type($this->mockUser->name, '#username')
66 + ->type($this->mockUser->password, '#password')
67 + ->press('Sign In')
68 + ->seePageIs('/')
69 + ->see($this->mockUser->name)
70 + ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
71 + }
72 +
49 public function test_initial_incorrect_details() 73 public function test_initial_incorrect_details()
50 { 74 {
51 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); 75 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
...@@ -55,7 +79,7 @@ class LdapTest extends \TestCase ...@@ -55,7 +79,7 @@ class LdapTest extends \TestCase
55 ->andReturn(['count' => 1, 0 => [ 79 ->andReturn(['count' => 1, 0 => [
56 'uid' => [$this->mockUser->name], 80 'uid' => [$this->mockUser->name],
57 'cn' => [$this->mockUser->name], 81 'cn' => [$this->mockUser->name],
58 - 'dn' => ['dc=test'.config('services.ldap.base_dn')] 82 + 'dn' => ['dc=test' . config('services.ldap.base_dn')]
59 ]]); 83 ]]);
60 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false); 84 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
61 85
......