From: Dan Brown Date: Sun, 14 Aug 2016 12:09:44 +0000 (+0100) Subject: Merge branch 'v0.11' X-Git-Tag: v0.12.0~1^2~12 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/92d393537c9de537d03141d8630d46fbe890f575?hp=-c Merge branch 'v0.11' --- 92d393537c9de537d03141d8630d46fbe890f575 diff --combined .travis.yml index 83e9e10f5,83e9e10f5..bea8d1795 --- a/.travis.yml +++ b/.travis.yml @@@ -25,6 -25,6 +25,7 @@@ before_script - composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN - phpenv config-rm xdebug.ini - composer self-update ++ - composer dump-autoload --no-interaction - composer install --prefer-dist --no-interaction - npm install - ./node_modules/.bin/gulp diff --combined app/Repos/PageRepo.php index d3b71cebd,3698e5efb..235246f82 --- a/app/Repos/PageRepo.php +++ b/app/Repos/PageRepo.php @@@ -157,8 -157,6 +157,8 @@@ class PageRepo extends EntityRep $draftPage->draft = false; $draftPage->save(); + $this->saveRevision($draftPage, 'Initial Publish'); + return $draftPage; } @@@ -310,9 -308,10 +310,9 @@@ */ public function updatePage(Page $page, $book_id, $input) { - // Save a revision before updating - if ($page->html !== $input['html'] || $page->name !== $input['name']) { - $this->saveRevision($page); - } + // Hold the old details to compare later + $oldHtml = $page->html; + $oldName = $page->name; // Prevent slug being updated if no name change if ($page->name !== $input['name']) { @@@ -336,11 -335,6 +336,11 @@@ // Remove all update drafts for this user & page. $this->userUpdateDraftsQuery($page, $userId)->delete(); + // Save a revision after updating + if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) { + $this->saveRevision($page, $input['summary']); + } + return $page; } @@@ -366,10 -360,9 +366,10 @@@ /** * Saves a page revision into the system. * @param Page $page + * @param null|string $summary * @return $this */ - public function saveRevision(Page $page) + public function saveRevision(Page $page, $summary = null) { $revision = $this->pageRevision->fill($page->toArray()); if (setting('app-editor') !== 'markdown') $revision->markdown = ''; @@@ -379,7 -372,6 +379,7 @@@ $revision->created_by = auth()->user()->id; $revision->created_at = $page->updated_at; $revision->type = 'version'; + $revision->summary = $summary; $revision->save(); // Clear old revisions if ($this->pageRevision->where('page_id', '=', $page->id)->count() > 50) { @@@ -599,14 -591,15 +599,15 @@@ /** * Gets a suitable slug for the resource - * @param $name - * @param $bookId + * @param string $name + * @param int $bookId * @param bool|false $currentId * @return string */ public function findSuitableSlug($name, $bookId, $currentId = false) { $slug = Str::slug($name); + if ($slug === "") $slug = substr(md5(rand(1, 500)), 0, 5); while ($this->doesSlugExist($slug, $bookId, $currentId)) { $slug .= '-' . substr(md5(rand(1, 500)), 0, 3); } diff --combined app/Services/ImageService.php index c10e989f9,d9bd61e9f..4401cb230 --- a/app/Services/ImageService.php +++ b/app/Services/ImageService.php @@@ -95,7 -95,6 +95,7 @@@ class ImageServic try { $storage->put($fullPath, $imageData); + $storage->setVisibility($fullPath, 'public'); } catch (Exception $e) { throw new ImageUploadException('Image Path ' . $fullPath . ' is not writable by the server.'); } @@@ -168,7 -167,6 +168,7 @@@ $thumbData = (string)$thumb->encode(); $storage->put($thumbFilePath, $thumbData); + $storage->setVisibility($thumbFilePath, 'public'); $this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72); return $this->getPublicUrl($thumbFilePath); @@@ -259,22 -257,16 +259,22 @@@ $storageUrl = config('filesystems.url'); // Get the standard public s3 url if s3 is set as storage type + // Uses the nice, short URL if bucket name has no periods in otherwise the longer + // region-based url will be used to prevent http issues. if ($storageUrl == false && config('filesystems.default') === 's3') { $storageDetails = config('filesystems.disks.s3'); - $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; + if (strpos($storageDetails['bucket'], '.') === false) { + $storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com'; + } else { + $storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket']; + } } $this->storageUrl = $storageUrl; } - return ($this->storageUrl == false ? '' : rtrim($this->storageUrl, '/')) . $filePath; + return ($this->storageUrl == false ? rtrim(baseUrl(''), '/') : rtrim($this->storageUrl, '/')) . $filePath; } -} +} diff --combined resources/assets/js/directives.js index e50f5c6dd,897707af5..fc7b88259 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@@ -1,10 -1,10 +1,10 @@@ "use strict"; - var DropZone = require('dropzone'); - var markdown = require('marked'); + const DropZone = require('dropzone'); + const markdown = require('marked'); - var toggleSwitchTemplate = require('./components/toggle-switch.html'); - var imagePickerTemplate = require('./components/image-picker.html'); - var dropZoneTemplate = require('./components/drop-zone.html'); + const toggleSwitchTemplate = require('./components/toggle-switch.html'); + const imagePickerTemplate = require('./components/image-picker.html'); + const dropZoneTemplate = require('./components/drop-zone.html'); module.exports = function (ngApp, events) { @@@ -54,7 -54,7 +54,7 @@@ imageClass: '@' }, link: function (scope, element, attrs) { - var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false'; + let usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false'; scope.image = scope.currentImage; scope.value = scope.currentImage || ''; if (usingIds) scope.value = scope.currentId; @@@ -80,7 -80,7 +80,7 @@@ }; scope.updateImageFromModel = function (model) { - var isResized = scope.resizeWidth && scope.resizeHeight; + let isResized = scope.resizeWidth && scope.resizeHeight; if (!isResized) { scope.$apply(() => { @@@ -89,8 -89,9 +89,9 @@@ return; } - var cropped = scope.resizeCrop ? 'true' : 'false'; - var requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped; + let cropped = scope.resizeCrop ? 'true' : 'false'; + let requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped; + requestString = window.baseUrl(requestString); $http.get(requestString).then((response) => { setImage(model, response.data.url); }); @@@ -157,22 -158,9 +158,22 @@@ return { restrict: 'A', link: function (scope, element, attrs) { - var menu = element.find('ul'); + const menu = element.find('ul'); element.find('[dropdown-toggle]').on('click', function () { menu.show().addClass('anim menuIn'); + let inputs = menu.find('input'); + let hasInput = inputs.length > 0; + if (hasInput) { + inputs.first().focus(); + element.on('keypress', 'input', event => { + if (event.keyCode === 13) { + event.preventDefault(); + menu.hide(); + menu.removeClass('anim menuIn'); + return false; + } + }); + } element.mouseleave(function () { menu.hide(); menu.removeClass('anim menuIn'); @@@ -265,11 -253,14 +266,14 @@@ link: function (scope, element, attrs) { // Set initial model content - var content = element.val(); + element = element.find('textarea').first(); + let content = element.val(); scope.mdModel = content; scope.mdChange(markdown(content)); - element.on('change input', (e) => { + console.log('test'); + + element.on('change input', (event) => { content = element.val(); $timeout(() => { scope.mdModel = content; @@@ -297,7 -288,7 +301,7 @@@ link: function (scope, element, attrs) { // Elements - const input = element.find('textarea[markdown-input]'); + const input = element.find('[markdown-input] textarea').first(); const display = element.find('.markdown-display').first(); const insertImage = element.find('button[data-action="insertImage"]'); @@@ -342,9 -333,9 +346,9 @@@ // Insert image shortcut if (event.which === 73 && event.ctrlKey && event.shiftKey) { event.preventDefault(); - var caretPos = input[0].selectionStart; - var currentContent = input.val(); - var mdImageText = "![](http://)"; + let caretPos = input[0].selectionStart; + let currentContent = input.val(); + const mdImageText = "![](http://)"; input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); input.focus(); input[0].selectionStart = caretPos + ("![](".length); @@@ -358,9 -349,9 +362,9 @@@ // Insert image from image manager insertImage.click(event => { window.ImageManager.showExternal(image => { - var caretPos = currentCaretPos; - var currentContent = input.val(); - var mdImageText = "![" + image.name + "](" + image.url + ")"; + let caretPos = currentCaretPos; + let currentContent = input.val(); + let mdImageText = "![" + image.name + "](" + image.url + ")"; input.val(currentContent.substring(0, caretPos) + mdImageText + currentContent.substring(caretPos)); input.change(); }); @@@ -634,7 -625,7 +638,7 @@@ // Get search url with correct types function getSearchUrl() { let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); - return `/ajax/search/entities?types=${types}`; + return window.baseUrl(`/ajax/search/entities?types=${types}`); } // Get initial contents diff --combined resources/assets/js/global.js index eeb1e4ea7,1c300ad26..3a107afa8 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@@ -7,15 -7,20 +7,23 @@@ var ngAnimate = require('angular-animat var ngSanitize = require('angular-sanitize'); require('angular-ui-sortable'); + // Url retrieval function + window.baseUrl = function(path) { + let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content'); + if (basePath[basePath.length-1] === '/') basePath = basePath.slice(0, basePath.length-1); + if (path[0] === '/') path = path.slice(1); + return basePath + '/' + path; + }; + var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']); // Global Event System -var Events = { - listeners: {}, - emit: function (eventName, eventData) { +class Events { + constructor() { + this.listeners = {}; + } + + emit(eventName, eventData) { if (typeof this.listeners[eventName] === 'undefined') return this; var eventsToStart = this.listeners[eventName]; for (let i = 0; i < eventsToStart.length; i++) { @@@ -23,16 -28,16 +31,17 @@@ event(eventData); } return this; - }, - listen: function (eventName, callback) { + } + + listen(eventName, callback) { if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = []; this.listeners[eventName].push(callback); return this; } }; -window.Events = Events; +window.Events = new Events(); + var services = require('./services')(ngApp, Events); var directives = require('./directives')(ngApp, Events); var controllers = require('./controllers')(ngApp, Events); @@@ -42,15 -47,14 +51,15 @@@ // Smooth scrolling jQuery.fn.smoothScrollTo = function () { if (this.length === 0) return; - $('body').animate({ + let scrollElem = document.documentElement.scrollTop === 0 ? document.body : document.documentElement; + $(scrollElem).animate({ scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin }, 800); // Adjust to change animations speed (ms) return this; }; // Making contains text expression not worry about casing -$.expr[":"].contains = $.expr.createPseudo(function (arg) { +jQuery.expr[":"].contains = $.expr.createPseudo(function (arg) { return function (elem) { return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; }; @@@ -100,14 -104,13 +109,14 @@@ $(function () var scrollTop = document.getElementById('back-to-top'); var scrollTopBreakpoint = 1200; window.addEventListener('scroll', function() { - if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) { + let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; + if (!scrollTopShowing && scrollTopPos > scrollTopBreakpoint) { scrollTop.style.display = 'block'; scrollTopShowing = true; setTimeout(() => { scrollTop.style.opacity = 0.4; }, 1); - } else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) { + } else if (scrollTopShowing && scrollTopPos < scrollTopBreakpoint) { scrollTop.style.opacity = 0; scrollTopShowing = false; setTimeout(() => { diff --combined resources/assets/js/pages/page-form.js index b2d5f0c5c,f8b314e9c..86678a1ba --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@@ -1,65 -1,8 +1,65 @@@ +"use strict"; + +function editorPaste(e) { + if (!e.clipboardData) return + var items = e.clipboardData.items; + if (!items) return; + for (var i = 0; i < items.length; i++) { + if (items[i].type.indexOf("image") !== -1) { + + var file = items[i].getAsFile(); + var formData = new FormData(); + var ext = 'png'; + var xhr = new XMLHttpRequest(); + + if (file.name) { + var fileNameMatches = file.name.match(/\.(.+)$/); + if (fileNameMatches) { + ext = fileNameMatches[1]; + } + } + + var id = "image-" + Math.random().toString(16).slice(2); + editor.execCommand('mceInsertContent', false, ''); + + var remoteFilename = "image-" + Date.now() + "." + ext; + formData.append('file', file, remoteFilename); + formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content')); + - xhr.open('POST', '/images/gallery/upload'); ++ xhr.open('POST', window.baseUrl('/images/gallery/upload')); + xhr.onload = function () { + if (xhr.status === 200 || xhr.status === 201) { + var result = JSON.parse(xhr.responseText); + editor.dom.setAttrib(id, 'src', result.url); + } else { + console.log('An error occured uploading the image'); + console.log(xhr.responseText); + editor.dom.remove(id); + } + }; + xhr.send(formData); + } + } +} + +function registerEditorShortcuts(editor) { + // Headers + for (let i = 1; i < 5; i++) { + editor.addShortcut('ctrl+' + i, '', ['FormatBlock', false, 'h' + i]); + } + + // Other block shortcuts + editor.addShortcut('ctrl+q', '', ['FormatBlock', false, 'blockquote']); + editor.addShortcut('ctrl+d', '', ['FormatBlock', false, 'p']); + editor.addShortcut('ctrl+e', '', ['FormatBlock', false, 'pre']); + editor.addShortcut('ctrl+s', '', ['FormatBlock', false, 'code']); +} + var mceOptions = module.exports = { selector: '#html-editor', content_css: [ - '/css/styles.css', - '/libs/material-design-iconic-font/css/material-design-iconic-font.min.css' + window.baseUrl('/css/styles.css'), + window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css') ], body_class: 'page-content', relative_urls: false, @@@ -123,8 -66,6 +123,8 @@@ mceOptions.extraSetups[i](editor); } + registerEditorShortcuts(editor); + (function () { var wrap; @@@ -181,6 -122,49 +181,6 @@@ }); // Paste image-uploads - editor.on('paste', function (e) { - if (e.clipboardData) { - var items = e.clipboardData.items; - if (items) { - for (var i = 0; i < items.length; i++) { - if (items[i].type.indexOf("image") !== -1) { - - var file = items[i].getAsFile(); - var formData = new FormData(); - var ext = 'png'; - var xhr = new XMLHttpRequest(); - - if (file.name) { - var fileNameMatches = file.name.match(/\.(.+)$/); - if (fileNameMatches) { - ext = fileNameMatches[1]; - } - } - - var id = "image-" + Math.random().toString(16).slice(2); - editor.execCommand('mceInsertContent', false, ''); - - var remoteFilename = "image-" + Date.now() + "." + ext; - formData.append('file', file, remoteFilename); - formData.append('_token', document.querySelector('meta[name="token"]').getAttribute('content')); - - xhr.open('POST', window.baseUrl('/images/gallery/upload')); - xhr.onload = function () { - if (xhr.status === 200 || xhr.status === 201) { - var result = JSON.parse(xhr.responseText); - editor.dom.setAttrib(id, 'src', result.url); - } else { - console.log('An error occured uploading the image'); - console.log(xhr.responseText); - editor.dom.remove(id); - } - }; - xhr.send(formData); - } - } - } - - } - }); + editor.on('paste', editorPaste); } }; diff --combined resources/views/pages/form.blade.php index a5558f8e4,18a9868c7..366316b33 --- a/resources/views/pages/form.blade.php +++ b/resources/views/pages/form.blade.php @@@ -20,27 -20,16 +20,27 @@@ Save Draft
  • - Delete Draft + Delete Draft
  • +
  • + Discard Draft +
  • + - - +
    @@@ -55,8 -44,11 +55,11 @@@
    @if(setting('app-editor') === 'wysiwyg') - +
    + +
    + @if($errors->has('html'))
    {{ $errors->first('html') }}
    @endif @@@ -72,8 -64,12 +75,12 @@@
    - + +
    + +
    +
    diff --combined resources/views/pages/restrictions.blade.php index 8dccc021e,8eca486c3..bd88919df --- a/resources/views/pages/restrictions.blade.php +++ b/resources/views/pages/restrictions.blade.php @@@ -7,16 -7,16 +7,16 @@@ diff --combined resources/views/pages/revisions.blade.php index d92b97d62,03fb23673..926affffc --- a/resources/views/pages/revisions.blade.php +++ b/resources/views/pages/revisions.blade.php @@@ -5,59 -5,45 +5,59 @@@
    -
    + +

    Page Revisions For "{{ $page->name }}"

    @if(count($page->revisions) > 0) - - - - + + + + + - @foreach($page->revisions as $revision) + @foreach($page->revisions as $index => $revision) -- ++ -- -- - - ++ ++ ++ + @if ($index !== 0) + + @else + + @endif @endforeach
    NameCreated ByRevision DateActionsNameCreated ByRevision DateChangelogActions
    {{$revision->name}}{{ $revision->name }} @if($revision->createdBy) -- {{$revision->createdBy->name}} ++ {{ $revision->createdBy->name }} @endif @if($revision->createdBy) {{$revision->createdBy->name}} @else Deleted User @endif{{$revision->created_at->format('jS F, Y H:i:s')}}
    ({{$revision->created_at->diffForHumans()}})
    {{$revision->summary}} - Preview -  |  - Restore - @if($revision->createdBy) {{ $revision->createdBy->name }} @else Deleted User @endif{{ $revision->created_at->format('jS F, Y H:i:s') }}
    ({{ $revision->created_at->diffForHumans() }})
    {{ $revision->summary }} - Preview ++ Preview +  |  - Restore ++ Restore + Current Version
    diff --combined tests/Entity/EntityTest.php index 8c0c286a6,71d83dd47..296aa72ed --- a/tests/Entity/EntityTest.php +++ b/tests/Entity/EntityTest.php @@@ -151,8 -151,10 +151,10 @@@ class EntityTest extends TestCas ->visit('/books/create') ->type($book->name, '#name') ->type($book->description, '#description') - ->press('Save Book') - ->seePageIs('/books/my-first-book-2'); + ->press('Save Book'); + + $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; + $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); $book = \BookStack\Book::where('slug', '=', 'my-first-book')->first(); return $book; @@@ -216,24 -218,13 +218,24 @@@ public function test_old_page_slugs_redirect_to_new_pages() { - $page = \BookStack\Page::all()->first(); + $page = \BookStack\Page::first(); $pageUrl = $page->getUrl(); $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; + // Need to save twice since revisions are not generated in seeder. $this->asAdmin()->visit($pageUrl) + ->clickInElement('#content', 'Edit') + ->type('super test', '#name') + ->press('Save Page'); + + $page = \BookStack\Page::first(); + $pageUrl = $page->getUrl(); + + // Second Save + $this->visit($pageUrl) ->clickInElement('#content', 'Edit') ->type('super test page', '#name') ->press('Save Page') + // Check redirect ->seePageIs($newPageUrl) ->visit($pageUrl) ->seePageIs($newPageUrl);