Merge branch 'master' into release

This commit is contained in:
Dan Brown 2019-05-27 13:47:47 +01:00
commit 33a04697ef
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
40 changed files with 442 additions and 237 deletions

View File

@ -216,12 +216,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) {
if (mb_strlen($this->name) <= $chars) {
return $this->name;
}
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) {
if (mb_strlen($splitName[0]) <= $chars) {
return $splitName[0];
}

View File

@ -49,7 +49,7 @@ class CreateAdmin extends Command
if (empty($email)) {
$email = $this->ask('Please specify an email address for the new admin user');
}
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Invalid email address provided');
}
@ -61,7 +61,7 @@ class CreateAdmin extends Command
if (empty($name)) {
$name = $this->ask('Please specify an name for the new admin user');
}
if (strlen($name) < 2) {
if (mb_strlen($name) < 2) {
return $this->error('Invalid name provided');
}
@ -69,7 +69,7 @@ class CreateAdmin extends Command
if (empty($password)) {
$password = $this->secret('Please specify a password for the new admin user');
}
if (strlen($password) < 5) {
if (mb_strlen($password) < 5) {
return $this->error('Invalid password provided, Must be at least 5 characters');
}

View File

@ -104,7 +104,7 @@ class Book extends Entity
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**

View File

@ -83,7 +83,7 @@ class Bookshelf extends Entity
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**

View File

@ -56,7 +56,7 @@ class Chapter extends Entity
public function getExcerpt(int $length = 100)
{
$description = $this->text ?? $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**

View File

@ -852,11 +852,14 @@ class EntityRepo
*/
public function destroyPage(Page $page)
{
// Check if set as custom homepage
// Check if set as custom homepage & remove setting if not used or throw error if active
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
setting()->remove('app-homepage');
}
$this->destroyEntityCommonRelations($page);

View File

@ -192,7 +192,7 @@ class PageRepo extends EntityRepo
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
@ -422,25 +422,29 @@ class PageRepo extends EntityRepo
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
$text = $header->nodeValue;
$tree->push([
$tree = collect($headers)->map(function($header) {
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
if (mb_strlen($text) > 30) {
$text = mb_substr($text, 0, 27) . '...';
}
return [
'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'),
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
]);
}
'text' => $text,
];
})->filter(function($header) {
return mb_strlen($header['text']) > 0;
});
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
}
return $tree->toArray();
}

View File

@ -142,7 +142,7 @@ class RegisterController extends Controller
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}

View File

@ -230,7 +230,7 @@ class ImageRepo
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150, false),
'display' => $this->getThumbnail($image, 840, null, true)
'display' => $this->getThumbnail($image, 1680, null, true)
];
}

View File

@ -123,18 +123,19 @@ function baseUrl($path, $forceAppDomain = false)
// Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) {
if (!empty($base) && strpos($path, $base) === 0) {
$path = trim(substr($path, strlen($base) - 1));
}
$path = mb_substr($path, mb_strlen($base));
} else {
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
}
// Return normal url path if not specified in config
if (config('app.url') === '') {
return url($path);
}
return $base . '/' . $path;
return $base . '/' . ltrim($path, '/');
}
/**

View File

@ -180,9 +180,20 @@ class MarkdownEditor {
// Handle image paste
cm.on('paste', (cm, event) => {
if (!event.clipboardData || !event.clipboardData.items) return;
for (let i = 0; i < event.clipboardData.items.length; i++) {
uploadImage(event.clipboardData.items[i].getAsFile());
const clipboardItems = event.clipboardData.items;
if (!event.clipboardData || !clipboardItems) return;
// Don't handle if clipboard includes text content
for (let clipboardItem of clipboardItems) {
if (clipboardItem.type.includes('text/')) {
return;
}
}
for (let clipboardItem of clipboardItems) {
if (clipboardItem.type.includes("image")) {
uploadImage(clipboardItem.getAsFile());
}
}
});

View File

@ -20,6 +20,7 @@ class PageDisplay {
// Sidebar page nav click event
$('.sidebar-page-nav').on('click', 'a', event => {
window.components['tri-layout'][0].showContent();
this.goToText(event.target.getAttribute('href').substr(1));
});
}

View File

@ -66,28 +66,44 @@ class TriLayout {
*/
mobileTabClick(event) {
const tab = event.target.getAttribute('tri-layout-mobile-tab');
this.showTab(tab);
}
/**
* Show the content tab.
* Used by the page-display component.
*/
showContent() {
this.showTab('content');
}
/**
* Show the given tab
* @param tabName
*/
showTab(tabName) {
this.scrollCache[this.lastTabShown] = document.documentElement.scrollTop;
// Set tab status
const activeTabs = document.querySelectorAll('.tri-layout-mobile-tab.active');
for (let tab of activeTabs) {
tab.classList.remove('active');
const tabs = document.querySelectorAll('.tri-layout-mobile-tab');
for (let tab of tabs) {
const isActive = (tab.getAttribute('tri-layout-mobile-tab') === tabName);
tab.classList.toggle('active', isActive);
}
event.target.classList.add('active');
// Toggle section
const showInfo = (tab === 'info');
const showInfo = (tabName === 'info');
this.elem.classList.toggle('show-info', showInfo);
// Set the scroll position from cache
const pageHeader = document.querySelector('header');
const defaultScrollTop = pageHeader.getBoundingClientRect().bottom;
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
setTimeout(() => {
document.documentElement.scrollTop = this.scrollCache[tab] || defaultScrollTop;
document.documentElement.scrollTop = this.scrollCache[tabName] || defaultScrollTop;
}, 50);
this.lastTabShown = tab;
this.lastTabShown = tabName;
}
}

View File

@ -8,11 +8,20 @@ import DrawIO from "../services/drawio";
* @param editor
*/
function editorPaste(event, editor, wysiwygComponent) {
if (!event.clipboardData || !event.clipboardData.items) return;
const clipboardItems = event.clipboardData.items;
if (!event.clipboardData || !clipboardItems) return;
for (let clipboardItem of event.clipboardData.items) {
if (clipboardItem.type.indexOf("image") === -1) continue;
event.preventDefault();
// Don't handle if clipboard includes text content
for (let clipboardItem of clipboardItems) {
if (clipboardItem.type.includes('text/')) {
return;
}
}
for (let clipboardItem of clipboardItems) {
if (!clipboardItem.type.includes("image")) {
continue;
}
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');

View File

@ -181,7 +181,7 @@
margin-left: auto;
margin-right: auto;
margin-bottom: $-xl;
overflow: auto;
overflow: initial;
min-height: 60vh;
&.auto-height {
min-height: 0;
@ -202,7 +202,7 @@
}
@include smaller-than($s) {
.content-wrap.card {
padding: $-m $-s;
padding: $-m $-m;
}
}

View File

@ -59,7 +59,7 @@
}
@include smaller-than($m) {
.grid.third {
.grid.third:not(.no-break) {
grid-template-columns: 1fr 1fr;
}
.grid.half:not(.no-break), .grid.left-focus:not(.no-break), .grid.right-focus:not(.no-break) {
@ -81,7 +81,7 @@
}
@include smaller-than($s) {
.grid.third {
.grid.third:not(.no-break) {
grid-template-columns: 1fr;
}
}
@ -257,10 +257,6 @@ body.flexbox {
padding-left: $-m;
padding-right: $-m;
}
.tri-layout-right-contents > div, .tri-layout-left-contents > div {
opacity: 0.6;
z-index: 0;
}
.tri-layout-left > *, .tri-layout-right > * {
display: none;
pointer-events: none;
@ -298,6 +294,13 @@ body.flexbox {
.tri-layout-mobile-tabs {
display: none;
}
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms;
&:hover {
opacity: 1;
}
}
}
@include smaller-than($m) {
@ -306,11 +309,3 @@ body.flexbox {
margin-right: 0;
}
}
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms;
&:hover {
opacity: 1;
}
}

View File

@ -164,15 +164,21 @@
padding-left: 1rem;
padding-right: 0;
}
.entity-list-item {
padding-top: $-xxs;
padding-bottom: $-xxs;
background-clip: content-box;
border-radius: 0 3px 3px 0;
.content {
padding-top: $-xs;
padding-bottom: $-xs;
max-width: calc(100% - 20px);
}
}
.entity-list-item.selected {
background-color: rgba(0, 0, 0, 0.08);
}
.entity-list-item.no-hover {
margin-top: -$-xs;
padding-right: 0;

View File

@ -20,11 +20,10 @@
}
}
@include smaller-than($m) {
@include smaller-than($s) {
.page-edit-toolbar {
overflow-x: scroll;
overflow-y: visible;
z-index: 12;
}
.page-edit-toolbar .grid.third {
display: block;
@ -35,8 +34,7 @@
}
}
@include smaller-than($m) {
.page-edit-toolbar #save-button {
.page-save-mobile-button {
position: fixed;
z-index: 30;
border-radius: 50%;
@ -47,12 +45,10 @@
bottom: $-s;
box-shadow: $bs-hover;
background-color: currentColor;
text-align: center;
svg {
fill: #FFF;
}
span {
display: none;
}
margin-right: 0;
}
}

View File

@ -291,15 +291,27 @@ li.checkbox-item, li.task-list-item {
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
@each $sizeLetter, $size in $screen-sizes {
@include larger-than($size) {
.text-#{$sizeLetter}-center {
text-align: center;
}
.text-#{$sizeLetter}-left {
text-align: left;
}
.text-#{$sizeLetter}-right {
text-align: right;
}
}
}
.text-bigger {
font-size: 1.1em;
}

View File

@ -37,6 +37,14 @@ return [
'book_sort' => 'sorteerde boek',
'book_sort_notification' => 'Boek Succesvol Gesorteerd',
// Bookshelves
'bookshelf_create' => 'maakte Boekenplank',
'bookshelf_create_notification' => 'Boekenplank Succesvol Aangemaakt',
'bookshelf_update' => 'veranderde boekenplank',
'bookshelf_update_notification' => 'Boekenplank Succesvol Bijgewerkt',
'bookshelf_delete' => 'verwijderde boekenplank',
'bookshelf_delete_notification' => 'Boekenplank Succesvol Verwijderd',
// Other
'commented_on' => 'reactie op',
];

View File

@ -1,35 +1,35 @@
<?php
return [
/**
* Buttons
*/
// Buttons
'cancel' => 'Annuleren',
'confirm' => 'Bevestigen',
'back' => 'Terug',
'save' => 'Opslaan',
'continue' => 'Doorgaan',
'select' => 'Kies',
'toggle_all' => 'Toggle Alles',
'more' => 'Meer',
/**
* Form Labels
*/
// Form Labels
'name' => 'Naam',
'description' => 'Beschrijving',
'role' => 'Rol',
'cover_image' => 'Omslagfoto',
'cover_image_description' => 'Deze afbeelding moet ongeveer 300x170px zijn.',
/**
* Actions
*/
// Actions
'actions' => 'Acties',
'view' => 'Bekijk',
'view_all' => 'Bekijk Alle',
'create' => 'Aanmaken',
'update' => 'Update',
'edit' => 'Bewerk',
'sort' => 'Sorteer',
'move' => 'Verplaats',
'copy' => 'Kopiëren',
'reply' => 'Beantwoorden',
'delete' => 'Verwijder',
'search' => 'Zoek',
'search_clear' => 'Zoekopdracht wissen',
@ -37,15 +37,22 @@ return [
'remove' => 'Verwijderen',
'add' => 'Toevoegen',
/**
* Misc
*/
// Sort Options
'sort_name' => 'Naam',
'sort_created_at' => 'Aanmaakdatum',
'sort_updated_at' => 'Gewijzigd op',
// Misc
'deleted_user' => 'Verwijderde gebruiker',
'no_activity' => 'Geen activiteiten',
'no_items' => 'Geen items beschikbaar',
'back_to_top' => 'Terug naar boven',
'toggle_details' => 'Details Weergeven',
'toggle_thumbnails' => 'Thumbnails Weergeven',
'details' => 'Details',
'grid_view' => 'Grid weergave',
'list_view' => 'Lijst weergave',
'default' => 'Standaard',
/**
* Header
@ -53,6 +60,10 @@ return [
'view_profile' => 'Profiel Weergeven',
'edit_profile' => 'Profiel Bewerken',
// Layout tabs
'tab_info' => 'Info',
'tab_content' => 'Inhoud',
/**
* Email Content
*/

View File

@ -65,9 +65,38 @@ return [
'search_set_date' => 'Zet datum',
'search_update' => 'Update zoekresultaten',
/**
* Books
*/
// Shelves
'shelf' => 'Boekenplank',
'shelves' => 'Boekenplanken',
'x_shelves' => ':count Boekenplank|:count Boekenplanken',
'shelves_long' => 'Boekenplanken',
'shelves_empty' => 'Er zijn geen boekenplanken aangemaakt',
'shelves_create' => 'Nieuwe Boekenplank Aanmaken',
'shelves_popular' => 'Populaire Boekenplanken',
'shelves_new' => 'Nieuwe Boekenplanken',
'shelves_popular_empty' => 'De meest populaire boekenplanken worden hier weergegeven.',
'shelves_new_empty' => 'De meest recent aangemaakt boekenplanken worden hier weergeven.',
'shelves_save' => 'Boekenplanken Opslaan',
'shelves_books' => 'Boeken op deze plank',
'shelves_add_books' => 'Toevoegen boeken aan deze plank',
'shelves_drag_books' => 'Sleep boeken hier naartoe om deze toe te voegen aan deze plank',
'shelves_empty_contents' => 'Er zijn geen boeken aan deze plank toegekend',
'shelves_edit_and_assign' => 'Bewerk boekenplank om boeken toe te kennen.',
'shelves_edit_named' => 'Bewerk Boekenplank :name',
'shelves_edit' => 'Bewerk Boekenplank',
'shelves_delete' => 'Verwijder Boekenplank',
'shelves_delete_named' => 'Verwijder Boekenplank :name',
'shelves_delete_explain' => "Deze actie verwijdert de boekenplank met naam ':name'. De boeken op deze plank worden niet verwijderd.",
'shelves_delete_confirmation' => 'Weet je zeker dat je deze boekenplank wilt verwijderen?',
'shelves_permissions' => 'Boekenplank Permissies',
'shelves_permissions_updated' => 'Boekenplank Permissies Opgeslagen',
'shelves_permissions_active' => 'Boekenplank Permissies Actief',
'shelves_copy_permissions_to_books' => 'Kopieer Permissies naar Boeken',
'shelves_copy_permissions' => 'Kopieer Permissies',
'shelves_copy_permissions_explain' => 'Met deze actie worden de permissies van deze boekenplank gekopieerd naar alle boeken op de plank. Voordat deze actie wordt uitgevoerd, zorg dat de wijzigingen in de permissies van deze boekenplank zijn opgeslagen.',
'shelves_copy_permission_success' => 'Boekenplank permissies gekopieerd naar :count boeken',
// Books
'book' => 'Boek',
'books' => 'Boeken',
'x_books' => ':count Boek|:count Boeken',

View File

@ -26,6 +26,7 @@ return [
*/
'actions' => 'Действия',
'view' => 'Просмотр',
'view_all' => 'Показать все',
'create' => 'Создание',
'update' => 'Обновление',
'edit' => 'Редактировать',
@ -40,18 +41,24 @@ return [
'remove' => 'Удалить',
'add' => 'Добавить',
// Sort Options
'sort_name' => 'По имени',
'sort_created_at' => 'По дате создания',
'sort_updated_at' => 'По дате обновления',
/**
* Misc
*/
'deleted_user' => 'Удаленный пользователь',
'no_activity' => 'Нет действий для просмотра',
'no_items' => 'Нет доступных элементов',
'back_to_top' => 'Вернуться наверх',
'back_to_top' => 'Наверх',
'toggle_details' => 'Подробности',
'toggle_thumbnails' => 'Миниатюры',
'details' => 'Детали',
'grid_view' => 'Вид сеткой',
'list_view' => 'Вид списком',
'default' => 'По умолчанию',
/**
* Header
@ -59,6 +66,10 @@ return [
'view_profile' => 'Просмотреть профиль',
'edit_profile' => 'Редактировать профиль',
// Layout tabs
'tab_info' => 'Информация',
'tab_content' => 'Содержание',
/**
* Email Content
*/

View File

@ -105,11 +105,13 @@ return [
*/
'shelf' => 'Полка',
'shelves' => 'Полки',
'x_shelves' => ':count полок|:count полок',
'shelves_long' => 'Книжные полки',
'shelves_empty' => 'Полки не созданы',
'shelves_create' => 'Создать новую полку',
'shelves_popular' => 'Популярные полки',
'shelves_new' => 'Новые полки',
'shelves_new_action' => 'Новая полка',
'shelves_popular_empty' => 'Популярные полки появятся здесь.',
'shelves_new_empty' => 'Последние созданные полки появятся здесь.',
'shelves_save' => 'Сохранить полку',

View File

@ -1,77 +1,73 @@
<?php
return [
/**
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
return [
// Common Messages
'settings' => 'Настройки',
'settings_save' => 'Сохранить настройки',
'settings_save_success' => 'Настройки сохранены',
/**
* App settings
*/
'app_settings' => 'Настройки приложения',
// App Settings
'app_customization' => 'Настройки',
'app_features_security' => 'Функции & Безопасность',
'app_name' => 'Имя приложения',
'app_name_desc' => 'Это имя отображается в заголовке и в любых письмах.',
'app_name_header' => 'Показать имя приложения в заголовке?',
'app_name_desc' => 'Имя отображается в заголовке email отправленных системой.',
'app_name_header' => 'Отображать имя приложения в заголовке',
'app_public_access' => 'Публичный доступ',
'app_public_access_desc' => 'Включение этой опции позволит неавторизованным посетителям получить доступ к содержимому вашего BookStack.',
'app_public_access_desc_guest' => 'Публичный доступ контролируется через настройки пользователя "Guest"',
'app_public_access_toggle' => 'Разрешить публичный доступ',
'app_public_viewing' => 'Разрешить публичный просмотр?',
'app_secure_images' => 'Включить загрузку изображений с повышенной безопасностью?',
'app_secure_images_desc' => 'Из соображений производительности все изображения являются общедоступными. Этот параметр добавляет случайную сложную строку перед образами изображений. Убедитесь, что индексация каталогов не включена, чтобы предотвратить к ним легкий доступ.',
'app_secure_images' => 'Загрузка изображений с высоким уровнем безопасности.',
'app_secure_images_toggle' => 'Включить загрузку изображений с высоким уровнем безопасности',
'app_secure_images_desc' => 'Для высокой производительности все изображения являются общедоступными. Этот параметр добавляет случайную строку перед URL изображения. Убедитесь, что индексация каталогов отключена, для предотвращения легкого доступа.',
'app_editor' => 'Редактор страницы',
'app_editor_desc' => 'Выберите, какой редактор будет использоваться всеми пользователями для редактирования страниц.',
'app_custom_html' => 'Пользовательский контент заголовка HTML',
'app_custom_html_desc' => 'Любой контент, добавленный здесь, будет вставлен в нижнюю часть раздела <head> каждой страницы. Это удобно для переопределения стилей или добавления кода аналитики.',
'app_logo' => 'Лого приложения',
'app_logo_desc' => 'Это изображение должно быть 43px в высоту. <br>Большое изображение будет уменьшено.',
'app_primary_color' => 'Главный цвет приложения',
'app_primary_color' => 'Основной цвет приложения',
'app_primary_color_desc' => 'Значение должно быть указано в hex-формате. <br>Оставьте пустым чтобы использовать цвет по умолчанию.',
'app_homepage' => 'Домашняя страница приложения',
'app_homepage' => 'Стартовая страница приложения',
'app_homepage_desc' => 'Выберите страницу, которая будет отображаться на главной странице вместо стандартной. Права на страницы игнорируются для выбранных страниц.',
'app_homepage_default' => 'Выбрана домашняя страница по умолчанию',
'app_homepage_books' => 'Или выберите страницу со списком книг в качестве главной страницы. Это будет иметь приоритет над любой другой страницей.',
'app_disable_comments' => 'Отключить комментарии',
'app_disable_comments_desc' => 'Отключить комментарии на всех страницах приложения. Существующие комментарии не отображаются.',
/**
* Registration settings
*/
'app_homepage_select' => 'Выберите страницу',
'app_disable_comments' => 'Отключение комментов',
'app_disable_comments_toggle' => 'Отключить комментарии',
'app_disable_comments_desc' => 'Отключение комментов на всех страницах. Существующие комментарии отображаться не будут.',
// Registration Settings
'reg_settings' => 'Настройки регистрации',
'reg_allow' => 'Открыть регистрацию?',
'reg_enable' => 'Разрешить регистрацияю',
'reg_enable_toggle' => 'Разрешить регистрацию',
'reg_enable_desc' => 'Если регистрация разрешена, пользователь сможет зарегистрироваться в системе самомтоятельно. При регистрации назначается роль пользователя по умолчанию',
'reg_default_role' => 'Роль пользователя по умолчанию после регистрации',
'reg_confirm_email' => 'Требуется подтверждение по электронной почте?',
'reg_email_confirmation' => 'Подтверждение электонной почты',
'reg_email_confirmation_toggle' => 'Требовать подтверждение по электронной почте',
'reg_confirm_email_desc' => 'Если используется ограничение по домену, подтверждение будет обязательно, а этот пункт проигнорирован.',
'reg_confirm_restrict_domain' => 'Ограничить регистрацию по домену',
'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых возможна регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свои адреса уже после регистрации.',
'reg_confirm_restrict_domain_placeholder' => 'Нет ограничений',
/**
* Maintenance settings
*/
'reg_confirm_restrict_domain_desc' => 'Введите список доменов почты через запятую, для которых разрешена регистрация. Пользователям будет отправлено письмо для подтверждения адреса перед входом в приложение. <br> Обратите внимание, что пользователи смогут изменить свои адреса уже после регистрации.',
'reg_confirm_restrict_domain_placeholder' => 'Без ограничений',
// Maintenance settings
'maint' => 'Обслуживание',
'maint_image_cleanup' => 'Очистка изображений',
'maint_image_cleanup_desc' => 'Сканирует содержимое страниц и предыдущих версий и определяет изображения, которые не используются. Убедитесь, что у вас есть резервная копия базы данных и папки изображений перед запуском этой функции.',
'maint_image_cleanup_ignore_revisions' => 'Пропускать изображения в версиях',
'maint_image_cleanup_run' => 'Запустить очистку',
'maint_image_cleanup_run' => 'Выполнить очистку',
'maint_image_cleanup_warning' => 'Найдено :count возможно бесполезных изображений. Вы уверены, что хотите удалить эти изображения?',
'maint_image_cleanup_success' => ':count возможно бесполезных изображений было найдено и удалено!',
'maint_image_cleanup_nothing_found' => 'Не найдено ни одного бесполезного изображения!',
/**
* Role settings
*/
// Role Settings
'roles' => 'Роли',
'role_user_roles' => 'Роли пользователя',
'role_create' => 'Создать новую роль',
'role_create_success' => 'Роль упешно создана',
'role_create' => 'Добавить роль',
'role_create_success' => 'Роль упешно добавлена',
'role_delete' => 'Удалить роль',
'role_delete_confirm' => 'Это удалит роль с именем \':roleName\'.',
'role_delete_users_assigned' => 'Эта роль назначена :userCount пользователям. Если вы хотите перенести их из этой роли, выберите новую роль ниже.',
@ -81,7 +77,7 @@ return [
'role_edit' => 'Редактировать роль',
'role_details' => 'Детали роли',
'role_name' => 'Имя роли',
'role_desc' => 'Короткое описание роли',
'role_desc' => 'Краткое описание роли',
'role_external_auth_id' => 'Внешние ID авторизации',
'role_system' => 'Системные разрешения',
'role_manage_users' => 'Управление пользователями',
@ -91,37 +87,43 @@ return [
'role_manage_settings' => 'Управление настройками приложения',
'role_asset' => 'Разрешение для активации',
'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.',
'role_asset_admins' => 'Администраторы автоматически получают доступ ко всему контенту, но эти опции могут отображать или скрывать параметры пользовательского интерфейса.',
'role_all' => 'Все',
'role_own' => 'Владелец',
'role_controlled_by_asset' => 'Регулируемые активацией они загружаются в',
'role_controlled_by_asset' => 'Контролируется активом, в который они загружены',
'role_save' => 'Сохранить роль',
'role_update_success' => 'Роль успешно обновлена',
'role_users' => 'Пользователи с данной ролью',
'role_users_none' => 'Нет пользователей с данной ролью',
/**
* Users
*/
// Users
'users' => 'Пользователи',
'user_profile' => 'Профиль пользователя',
'users_add_new' => 'Добавить нового пользователя',
'users_add_new' => 'Добавить пользователя',
'users_search' => 'Поиск пользователей',
'users_details' => 'Данные пользователя',
'users_details_desc' => 'Задайте имя для этого пользователя, чтобы другие могли его узнать',
'users_details_desc_no_email' => 'Задайте имя для этого пользователя, чтобы другие могли его узнать',
'users_role' => 'Роли пользователя',
'users_role_desc' => 'Назначьте роли пользователю. Если назначено несколько ролей, разрешения будут суммироваться и пользователь получит все права назначенных ролей.',
'users_password' => 'Пароль пользователя',
'users_password_desc' => 'Установите пароль для входа в приложение. Должно быть не менее 5 символов.',
'users_external_auth_id' => 'Внешний ID аутентификации',
'users_password_warning' => 'Введите ниже свой пароль новый пароль для его изменения:',
'users_external_auth_id_desc' => 'Этот ID используется для связи с вашей LDAP системой.',
'users_password_warning' => 'Заполните ниже только если вы хотите сменить свой пароль.',
'users_system_public' => 'Этот пользователь представляет любых гостевых пользователей, которые посещают ваше приложение. Он не может использоваться для входа в систему и назначается автоматически.',
'users_delete' => 'Удалить пользователя',
'users_delete_named' => 'Удалить пользователя :userName',
'users_delete_warning' => 'Это полностью удалит этого пользователя с именем \':userName\' из системы.',
'users_delete_warning' => 'Это полностью удалит пользователя с именем \':userName\' из системы.',
'users_delete_confirm' => 'Вы уверены что хотите удалить этого пользователя?',
'users_delete_success' => 'Пользователи успешно удалены',
'users_edit' => 'Редактировать польщователя',
'users_edit' => 'Редактировать пользователя',
'users_edit_profile' => 'Редактировать профиль',
'users_edit_success' => 'Пользователь успешно обновлен',
'users_avatar' => 'Аватар пользователя',
'users_avatar_desc' => 'Это изображение должно быть размером около 256px.',
'users_avatar_desc' => 'Выберите изображение. Изображение должно быть квадратным, размером около 256px.',
'users_preferred_language' => 'Предпочитаемый язык',
'users_preferred_language_desc' => 'Этот параметр изменит язык интерфейса приложения. Это не влияет на созданный пользователем контент.',
'users_social_accounts' => 'Аккаунты Соцсетей',
'users_social_accounts_info' => 'Здесь вы можете подключить другие учетные записи для более быстрого и легкого входа в систему. Отключение учетной записи здесь не разрешено. Отменить доступ к настройкам вашего профиля в подключенном социальном аккаунте.',
'users_social_connect' => 'Подключить аккаунт',

View File

@ -58,7 +58,7 @@
<h1 id="{{$bookChild->getType()}}-{{$bookChild->id}}">{{ $bookChild->name }}</h1>
@if($bookChild->isA('chapter'))
<p>{{ $bookChild->description }}</p>
<p>{{ $bookChild->text }}</p>
@if(count($bookChild->pages) > 0)
@foreach($bookChild->pages as $page)

View File

@ -1,7 +1,5 @@
@extends('tri-layout')
@section('container-classes', 'mt-xl')
@section('body')
@include('books.list', ['books' => $books, 'view' => $view])
@stop

View File

@ -1,8 +1,8 @@
<div class="content-wrap mt-m card">
<div class="grid half v-center">
<div class="grid half v-center no-row-gap">
<h1 class="list-heading">{{ trans('entities.books') }}</h1>
<div class="text-right">
<div class="text-m-right my-m">
@include('partials.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'books'])

View File

@ -1,8 +1,8 @@
<div page-comments page-id="{{ $page->id }}" class="comments-list">
<div comment-count-bar class="grid half left-focus v-center">
<div comment-count-bar class="grid half left-focus v-center no-row-gap">
<h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
@if (count($page->comments) === 0)
<div class="text-right" comment-add-button-container>
<div class="text-m-right" comment-add-button-container>
<button type="button" action="addComment"
class="button outline">{{ trans('entities.comment_add') }}</button>
</div>

View File

@ -1,10 +1,14 @@
@extends('simple-layout')
@extends('tri-layout')
@section('body')
<div class="container mt-m">
<div class="grid right-focus gap-xl">
<div>
@include('books.list', ['books' => $books, 'view' => $view])
@stop
@section('left')
@include('common.home-sidebar')
@stop
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@ -12,12 +16,4 @@
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@include('common.home-sidebar')
</div>
<div>
@include('books.list', ['books' => $books, 'view' => $view])
</div>
</div>
</div>
@stop

View File

@ -1,26 +1,24 @@
@extends('simple-layout')
@extends('tri-layout')
@section('body')
<div class="container mt-l">
<div class="grid right-focus gap-xl">
<div>
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@include('common.home-sidebar')
</div>
<div>
<div class="mt-m">
<div class="content-wrap card">
<div class="page-content" page-display="{{ $customHomepage->id }}">
@include('pages.page-display', ['page' => $customHomepage])
</div>
</div>
</div>
@stop
@section('left')
@include('common.home-sidebar')
@stop
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@stop

View File

@ -1,10 +1,14 @@
@extends('simple-layout')
@extends('tri-layout')
@section('body')
<div class="container mt-m">
<div class="grid right-focus gap-xl">
<div>
@include('shelves.list', ['shelves' => $shelves, 'view' => $view])
@stop
@section('left')
@include('common.home-sidebar')
@stop
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@ -12,12 +16,4 @@
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@include('common.home-sidebar')
</div>
<div>
@include('shelves.list', ['shelves' => $shelves, 'view' => $view])
</div>
</div>
</div>
@stop

View File

@ -12,7 +12,7 @@
{{--Header Bar--}}
<div class="primary-background-light toolbar page-edit-toolbar">
<div class="grid third v-center">
<div class="grid third no-break v-center">
<div class="action-buttons text-left px-m py-xs">
<a href="{{ back()->getTargetUrl() }}" class="text-button text-primary">@icon('back')<span class="hide-under-l">{{ trans('common.back') }}</span></a>
@ -49,7 +49,7 @@
<span>{{-- Prevents button jumping on menu show --}}</span>
</div>
<button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
<button type="submit" id="save-button" class="float-left text-primary text-button text-pos-hover hide-under-m">@icon('save')<span>{{ trans('entities.pages_save') }}</span></button>
</div>
</div>
</div>
@ -120,4 +120,6 @@
@endif
</div>
<button type="submit" id="save-button-mobile" title="{{ trans('entities.pages_save') }}" class="text-primary text-button hide-over-m page-save-mobile-button">@icon('save')</button>
</div>

View File

@ -190,6 +190,11 @@
<div>
<div v-pre class="card content-wrap">
<h1 class="list-heading">{{ trans('entities.search_results') }}</h1>
<form action="{{ baseUrl('/search') }}" method="GET" class="search-box flexible hide-over-l">
<input value="{{$searchTerm}}" type="text" name="term" placeholder="{{ trans('common.search') }}">
<button type="submit">@icon('search')</button>
<button v-if="searching" v-cloak class="search-box-cancel text-neg" v-on:click="clearSearch" type="button">@icon('close')</button>
</form>
<h6 class="text-muted">{{ trans_choice('entities.search_total_results_found', $totalResults, ['count' => $totalResults]) }}</h6>
<div class="book-contents">
@include('partials.entity-list', ['entities' => $entities, 'showPath' => true])

View File

@ -1,5 +1,6 @@
<?php namespace Tests;
use BookStack\Entities\Page;
use BookStack\Notifications\ConfirmEmail;
use BookStack\Auth\User;
use BookStack\Settings\SettingService;
@ -334,6 +335,17 @@ class AuthTest extends BrowserKitTest
->seeLink('Sign up');
}
public function test_login_redirects_to_initially_requested_url_correctly()
{
config()->set('app.url', 'http://localhost');
$page = Page::query()->first();
$this->visit($page->getUrl())
->seePageUrlIs(baseUrl('/login'));
$this->login('admin@admin.com', 'password')
->seePageUrlIs($page->getUrl());
}
/**
* Perform a login
* @param string $email

View File

@ -76,6 +76,20 @@ class ExportTest extends TestCase
$resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
}
public function test_book_html_export_shows_chapter_descriptions()
{
$chapterDesc = 'My custom test chapter description ' . str_random(12);
$chapter = Chapter::query()->first();
$chapter->description = $chapterDesc;
$chapter->save();
$book = $chapter->book;
$this->asEditor();
$resp = $this->get($book->getUrl('/export/html'));
$resp->assertSee($chapterDesc);
}
public function test_chapter_text_export()
{
$chapter = Chapter::first();

View File

@ -1,19 +0,0 @@
<?php namespace Tests;
class HelpersTest extends TestCase
{
public function test_base_url_takes_config_into_account()
{
config()->set('app.url', 'http://example.com/bookstack');
$result = baseUrl('/');
$this->assertEquals('http://example.com/bookstack/', $result);
}
public function test_base_url_takes_extra_path_into_account_on_forced_domain()
{
config()->set('app.url', 'http://example.com/bookstack');
$result = baseUrl('http://example.com/bookstack/', true);
$this->assertEquals('http://example.com/bookstack/', $result);
}
}

View File

@ -38,10 +38,14 @@ class HomepageTest extends TestCase
$name = 'My custom homepage';
$content = str_repeat('This is the body content of my custom homepage.', 20);
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings(['app-homepage' => $customPage->id]);
$this->setSettings([
'app-homepage' => $customPage->id,
'app-homepage-type' => 'page'
]);
$homeVisit = $this->get('/');
$homeVisit->assertSee($name);
$homeVisit->assertElementNotExists('#home-default');
$pageDeleteReq = $this->delete($customPage->getUrl());
$pageDeleteReq->assertStatus(302);
@ -54,6 +58,23 @@ class HomepageTest extends TestCase
$homeVisit->assertStatus(200);
}
public function test_custom_homepage_can_be_deleted_once_custom_homepage_no_longer_used()
{
$this->asEditor();
$name = 'My custom homepage';
$content = str_repeat('This is the body content of my custom homepage.', 20);
$customPage = $this->newPage(['name' => $name, 'html' => $content]);
$this->setSettings([
'app-homepage' => $customPage->id,
'app-homepage-type' => 'default'
]);
$pageDeleteReq = $this->delete($customPage->getUrl());
$pageDeleteReq->assertStatus(302);
$pageDeleteReq->assertSessionHas('success');
$pageDeleteReq->assertSessionMissing('error');
}
public function test_set_book_homepage()
{
$editor = $this->getEditor();

View File

@ -0,0 +1,33 @@
<?php namespace Tests;
class HelpersTest extends TestCase
{
public function test_base_url_takes_config_into_account()
{
config()->set('app.url', 'http://example.com/bookstack');
$result = baseUrl('/');
$this->assertEquals('http://example.com/bookstack/', $result);
}
public function test_base_url_takes_extra_path_into_account_on_forced_domain()
{
config()->set('app.url', 'http://example.com/bookstack');
$result = baseUrl('http://example.com/bookstack/', true);
$this->assertEquals('http://example.com/bookstack/', $result);
}
public function test_base_url_force_domain_works_as_expected_with_full_url_given()
{
config()->set('app.url', 'http://example.com');
$result = baseUrl('http://examps.com/books/test/page/cat', true);
$this->assertEquals('http://example.com/books/test/page/cat', $result);
}
public function test_base_url_force_domain_works_when_app_domain_is_same_as_given_url()
{
config()->set('app.url', 'http://example.com');
$result = baseUrl('http://example.com/books/test/page/cat', true);
$this->assertEquals('http://example.com/books/test/page/cat', $result);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Tests;
use BookStack\Entities\Repos\PageRepo;
class PageRepoTest extends TestCase
{
/**
* @var PageRepo $pageRepo
*/
protected $pageRepo;
protected function setUp()
{
parent::setUp();
$this->pageRepo = app()->make(PageRepo::class);
}
public function test_get_page_nav_does_not_show_empty_titles()
{
$content = '<h1 id="testa">Hello</h1><h2 id="testb">&nbsp;</h2><h3 id="testc"></h3>';
$navMap = $this->pageRepo->getPageNav($content);
$this->assertCount(1, $navMap);
$this->assertArraySubset([
'nodeName' => 'h1',
'link' => '#testa',
'text' => 'Hello'
], $navMap[0]);
}
}