diff --git a/.gitignore b/.gitignore
index 3f3dfdc3d..65c56ebbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,6 @@ Homestead.yaml
/public/plugins
/public/css/*.map
/public/js/*.map
-/public/uploads
/public/bower
/storage/images
_ide_helper.php
diff --git a/app/Exceptions/ConfirmationEmailException.php b/app/Exceptions/ConfirmationEmailException.php
index f343eff82..8736422c4 100644
--- a/app/Exceptions/ConfirmationEmailException.php
+++ b/app/Exceptions/ConfirmationEmailException.php
@@ -1,7 +1,4 @@
message);
return response()->redirectTo($e->redirectLocation);
}
+ // Handle pretty exceptions which will show a friendly application-fitting page
+ // Which will include the basic message to point the user roughly to the cause.
+ if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
+ $message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
+ return response()->view('errors/500', ['message' => $message], 500);
+ }
+
return parent::render($request, $e);
}
}
diff --git a/app/Exceptions/ImageUploadException.php b/app/Exceptions/ImageUploadException.php
index 205bdd4ff..6f4c73037 100644
--- a/app/Exceptions/ImageUploadException.php
+++ b/app/Exceptions/ImageUploadException.php
@@ -1,6 +1,3 @@
validate($request, [
- 'name' => 'required',
- 'email' => 'required|email|unique:users,email,' . $id,
+ 'name' => 'min:2',
+ 'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'role' => 'exists:roles,id'
diff --git a/app/Services/ImageService.php b/app/Services/ImageService.php
index 9d8aeea51..47c27cd0a 100644
--- a/app/Services/ImageService.php
+++ b/app/Services/ImageService.php
@@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
+use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@@ -119,10 +120,12 @@ class ImageService
* Checks the cache then storage to avoid creating / accessing the filesystem on every check.
*
* @param Image $image
- * @param int $width
- * @param int $height
- * @param bool $keepRatio
+ * @param int $width
+ * @param int $height
+ * @param bool $keepRatio
* @return string
+ * @throws Exception
+ * @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
@@ -139,8 +142,16 @@ class ImageService
return $this->getPublicUrl($thumbFilePath);
}
- // Otherwise create the thumbnail
- $thumb = $this->imageTool->make($storage->get($image->path));
+ try {
+ $thumb = $this->imageTool->make($storage->get($image->path));
+ } catch (Exception $e) {
+ if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
+ throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
+ } else {
+ throw $e;
+ }
+ }
+
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
diff --git a/app/Services/LdapService.php b/app/Services/LdapService.php
index 84883b09a..3d89e1e44 100644
--- a/app/Services/LdapService.php
+++ b/app/Services/LdapService.php
@@ -46,7 +46,7 @@ class LdapService
$user = $users[0];
return [
- 'uid' => $user['uid'][0],
+ 'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
diff --git a/database/migrations/2016_01_11_210908_add_external_auth_to_users.php b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php
index dda8f3d74..b7663054c 100644
--- a/database/migrations/2016_01_11_210908_add_external_auth_to_users.php
+++ b/database/migrations/2016_01_11_210908_add_external_auth_to_users.php
@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
$table->dropColumn('external_auth_id');
});
}
-}
+}
\ No newline at end of file
diff --git a/public/uploads/.gitignore b/public/uploads/.gitignore
new file mode 100644
index 000000000..c96a04f00
--- /dev/null
+++ b/public/uploads/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/readme.md b/readme.md
index b2f909efb..a191e1694 100644
--- a/readme.md
+++ b/readme.md
@@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS
## Requirements
-BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
+BookStack has similar requirements to Laravel:
* PHP >= 5.5.9, Will need to be usable from the command line.
-* OpenSSL PHP Extension
-* PDO PHP Extension
-* MBstring PHP Extension
-* Tokenizer PHP Extension
+* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
* MySQL >= 5.6
* Git (Not strictly required but helps manage updates)
* [Composer](https://getcomposer.org/)
-* [Node.js](https://nodejs.org/en/) **Development Only**
-* [Gulp](http://gulpjs.com/) **Development Only**
-
## Installation
@@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui
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.
-## Testing
+## Development & Testing
+
+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:
+
+* [Node.js](https://nodejs.org/en/) **Development Only**
+* [Gulp](http://gulpjs.com/) **Development Only**
+
+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.
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.
diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 5d8990754..76def6abd 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -1,6 +1,6 @@
"use strict";
-module.exports = function (ngApp) {
+module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
@@ -17,21 +17,40 @@ module.exports = function (ngApp) {
var dataLoaded = false;
var callback = false;
+ /**
+ * Simple returns the appropriate upload url depending on the image type set.
+ * @returns {string}
+ */
$scope.getUploadUrl = function () {
return '/images/' + $scope.imageType + '/upload';
};
+ /**
+ * Runs on image upload, Adds an image to local list of images
+ * and shows a success message to the user.
+ * @param file
+ * @param data
+ */
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
+ events.emit('success', 'Image uploaded');
};
+ /**
+ * Runs the callback and hides the image manager.
+ * @param returnData
+ */
function callbackAndHide(returnData) {
if (callback) callback(returnData);
$scope.showing = false;
}
+ /**
+ * Image select action. Checks if a double-click was fired.
+ * @param image
+ */
$scope.imageSelect = function (image) {
var dblClickTime = 300;
var currentTime = Date.now();
@@ -48,10 +67,19 @@ module.exports = function (ngApp) {
previousClickTime = currentTime;
};
+ /**
+ * Action that runs when the 'Select image' button is clicked.
+ * Runs the callback and hides the image manager.
+ */
$scope.selectButtonClick = function () {
callbackAndHide($scope.selectedImage);
};
+ /**
+ * Show the image manager.
+ * Takes a callback to execute later on.
+ * @param doneCallback
+ */
function show(doneCallback) {
callback = doneCallback;
$scope.showing = true;
@@ -62,6 +90,8 @@ module.exports = function (ngApp) {
}
}
+ // Connects up the image manger so it can be used externally
+ // such as from TinyMCE.
imageManagerService.show = show;
imageManagerService.showExternal = function (doneCallback) {
$scope.$apply(() => {
@@ -70,10 +100,16 @@ module.exports = function (ngApp) {
};
window.ImageManager = imageManagerService;
+ /**
+ * Hide the image manager
+ */
$scope.hide = function () {
$scope.showing = false;
};
+ /**
+ * Fetch the list image data from the server.
+ */
function fetchData() {
var url = '/images/' + $scope.imageType + '/all/' + page;
$http.get(url).then((response) => {
@@ -82,28 +118,33 @@ module.exports = function (ngApp) {
page++;
});
}
+ $scope.fetchData = fetchData;
+ /**
+ * Save the details of an image.
+ * @param event
+ */
$scope.saveImageDetails = function (event) {
event.preventDefault();
var url = '/images/update/' + $scope.selectedImage.id;
$http.put(url, this.selectedImage).then((response) => {
- $scope.imageUpdateSuccess = true;
- $timeout(() => {
- $scope.imageUpdateSuccess = false;
- }, 3000);
+ events.emit('success', 'Image details updated');
}, (response) => {
var errors = response.data;
var message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
- $scope.imageUpdateFailure = message;
- $timeout(() => {
- $scope.imageUpdateFailure = false;
- }, 5000);
+ events.emit('error', message);
});
};
+ /**
+ * Delete an image from system and notify of success.
+ * Checks if it should force delete when an image
+ * has dependant pages.
+ * @param event
+ */
$scope.deleteImage = function (event) {
event.preventDefault();
var force = $scope.dependantPages !== false;
@@ -112,10 +153,7 @@ module.exports = function (ngApp) {
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
- $scope.imageDeleteSuccess = true;
- $timeout(() => {
- $scope.imageDeleteSuccess = false;
- }, 3000);
+ events.emit('success', 'Image successfully deleted');
}, (response) => {
// Pages failure
if (response.status === 400) {
@@ -124,6 +162,15 @@ module.exports = function (ngApp) {
});
};
+ /**
+ * Simple date creator used to properly format dates.
+ * @param stringDate
+ * @returns {Date}
+ */
+ $scope.getDate = function(stringDate) {
+ return new Date(stringDate);
+ };
+
}]);
diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js
index 6ccfcf855..60abde6e9 100644
--- a/resources/assets/js/directives.js
+++ b/resources/assets/js/directives.js
@@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
var imagePickerTemplate = require('./components/image-picker.html');
var dropZoneTemplate = require('./components/drop-zone.html');
-module.exports = function (ngApp) {
+module.exports = function (ngApp, events) {
/**
* Toggle Switches
diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js
index 5b0b6f9f0..a61299d21 100644
--- a/resources/assets/js/global.js
+++ b/resources/assets/js/global.js
@@ -1,4 +1,4 @@
-
+"use strict";
// AngularJS - Create application and load components
var angular = require('angular');
@@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
var ngSanitize = require('angular-sanitize');
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
-var services = require('./services')(ngApp);
-var directives = require('./directives')(ngApp);
-var controllers = require('./controllers')(ngApp);
+
+
+// Global Event System
+var Events = {
+ listeners: {},
+ emit: function (eventName, eventData) {
+ if (typeof this.listeners[eventName] === 'undefined') return this;
+ var eventsToStart = this.listeners[eventName];
+ for (let i = 0; i < eventsToStart.length; i++) {
+ var event = eventsToStart[i];
+ event(eventData);
+ }
+ return this;
+ },
+ listen: function (eventName, callback) {
+ if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
+ this.listeners[eventName].push(callback);
+ return this;
+ }
+};
+window.Events = Events;
+
+var services = require('./services')(ngApp, Events);
+var directives = require('./directives')(ngApp, Events);
+var controllers = require('./controllers')(ngApp, Events);
//Global jQuery Config & Extensions
@@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
// Global jQuery Elements
$(function () {
+
+ var notifications = $('.notification');
+ var successNotification = notifications.filter('.pos');
+ var errorNotification = notifications.filter('.neg');
+ // Notification Events
+ window.Events.listen('success', function (text) {
+ successNotification.hide();
+ successNotification.find('span').text(text);
+ setTimeout(() => {
+ successNotification.show();
+ }, 1);
+ });
+ window.Events.listen('error', function (text) {
+ errorNotification.find('span').text(text);
+ errorNotification.show();
+ });
+
// Notification hiding
- $('.notification').click(function () {
+ notifications.click(function () {
$(this).fadeOut(100);
});
@@ -44,6 +83,29 @@ $(function () {
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
+ // Back to top button
+ $('#back-to-top').click(function() {
+ $('#header').smoothScrollTo();
+ });
+ var scrollTopShowing = false;
+ var scrollTop = document.getElementById('back-to-top');
+ var scrollTopBreakpoint = 1200;
+ window.addEventListener('scroll', function() {
+ if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
+ scrollTop.style.display = 'block';
+ scrollTopShowing = true;
+ setTimeout(() => {
+ scrollTop.style.opacity = 1;
+ }, 1);
+ } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
+ scrollTop.style.opacity = 0;
+ scrollTopShowing = false;
+ setTimeout(() => {
+ scrollTop.style.display = 'none';
+ }, 500);
+ }
+ });
+
});
diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js
index 756a9211b..290b7c653 100644
--- a/resources/assets/js/pages/page-form.js
+++ b/resources/assets/js/pages/page-form.js
@@ -8,7 +8,6 @@ module.exports = {
statusbar: false,
menubar: false,
paste_data_images: false,
- //height: 700,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
@@ -31,7 +30,7 @@ module.exports = {
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
},
file_browser_callback: function (field_name, url, type, win) {
- ImageManager.show(function (image) {
+ window.ImageManager.showExternal(function (image) {
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
@@ -40,6 +39,10 @@ module.exports = {
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
+ var html = '';
+ html += '';
+ html += '';
+ win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
});
},
paste_preprocess: function (plugin, args) {
diff --git a/resources/assets/js/services.js b/resources/assets/js/services.js
index 684a68450..cd2759c54 100644
--- a/resources/assets/js/services.js
+++ b/resources/assets/js/services.js
@@ -1,6 +1,6 @@
"use strict";
-module.exports = function(ngApp) {
+module.exports = function(ngApp, events) {
ngApp.factory('imageManagerService', function() {
return {
diff --git a/resources/assets/sass/_image-manager.scss b/resources/assets/sass/_image-manager.scss
index babbad0c1..8b18d24f3 100644
--- a/resources/assets/sass/_image-manager.scss
+++ b/resources/assets/sass/_image-manager.scss
@@ -21,7 +21,6 @@
border-radius: 4px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
overflow: hidden;
- max-width: 1340px;
position: fixed;
top: 0;
bottom: 0;
@@ -44,18 +43,49 @@
right: 0;
}
-.image-manager-list img {
+.image-manager-list .image {
display: block;
+ position: relative;
border-radius: 0;
float: left;
margin: 0;
cursor: pointer;
width: (100%/6);
height: auto;
- border: 1px solid #FFF;
+ border: 1px solid #DDD;
+ box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
+ overflow: hidden;
&.selected {
transform: scale3d(0.92, 0.92, 0.92);
+ border: 1px solid #444;
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
+ }
+ img {
+ width: 100%;
+ max-width: 100%;
+ display: block;
+ }
+ .image-meta {
+ position: absolute;
+ width: 100%;
+ bottom: 0;
+ left: 0;
+ color: #EEE;
+ background-color: rgba(0, 0, 0, 0.4);
+ font-size: 10px;
+ padding: 3px 4px;
+ span {
+ display: block;
+ }
+ }
+ @include smaller-than($xl) {
+ width: (100%/4);
+ }
+ @include smaller-than($m) {
+ .image-meta {
+ display: none;
+ }
}
}
diff --git a/resources/assets/sass/_lists.scss b/resources/assets/sass/_lists.scss
index fa609ee33..748694635 100644
--- a/resources/assets/sass/_lists.scss
+++ b/resources/assets/sass/_lists.scss
@@ -105,8 +105,8 @@
}
.book-tree .sidebar-page-list {
list-style: none;
- margin: 0;
- margin-top: $-xs;
+ margin: $-xs 0 0;
+ padding-left: 0;
border-left: 5px solid $color-book;
li a {
display: block;
diff --git a/resources/assets/sass/_text.scss b/resources/assets/sass/_text.scss
index d3663dee3..499896631 100644
--- a/resources/assets/sass/_text.scss
+++ b/resources/assets/sass/_text.scss
@@ -223,13 +223,13 @@ span.highlight {
* Lists
*/
ul {
- list-style: disc;
- margin-left: $-m*1.5;
+ padding-left: $-m * 1.5;
+ list-style: disc inside;
}
ol {
- list-style: decimal;
- margin-left: $-m*1.5;
+ list-style: decimal inside;
+ padding-left: $-m * 1.5;
}
/*
diff --git a/resources/assets/sass/export-styles.scss b/resources/assets/sass/export-styles.scss
index 90fdb196c..60450f3e2 100644
--- a/resources/assets/sass/export-styles.scss
+++ b/resources/assets/sass/export-styles.scss
@@ -9,4 +9,9 @@
@import "tables";
@import "header";
@import "lists";
-@import "pages";
\ No newline at end of file
+@import "pages";
+
+table {
+ border-spacing: 0;
+ border-collapse: collapse;
+}
\ No newline at end of file
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index c419c08b6..b81e43296 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -126,4 +126,42 @@ $loadingSize: 10px;
i {
padding-right: $-s;
}
+}
+
+// Back to top link
+$btt-size: 40px;
+#back-to-top {
+ background-color: rgba($primary, 0.4);
+ position: fixed;
+ bottom: $-m;
+ right: $-m;
+ padding: $-xs $-s;
+ cursor: pointer;
+ color: #FFF;
+ width: $btt-size;
+ height: $btt-size;
+ border-radius: $btt-size;
+ transition: all ease-in-out 180ms;
+ opacity: 0;
+ z-index: 999;
+ &:hover {
+ width: $btt-size*3.4;
+ background-color: rgba($primary, 1);
+ span {
+ display: inline-block;
+ }
+ }
+ .inner {
+ width: $btt-size*3.4;
+ }
+ i {
+ margin: 0;
+ font-size: 28px;
+ padding: 0 $-s 0 0;
+ }
+ span {
+ line-height: 12px;
+ position: relative;
+ top: -5px;
+ }
}
\ No newline at end of file
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index a2979f858..2dfa7ddfd 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -77,6 +77,11 @@
@yield('content')
+
{{ $message }}
+