Compare commits

...

42 Commits

Author SHA1 Message Date
Dan Brown
2558ea8931 Updated version for release v0.26.4 2019-08-06 21:42:09 +01:00
Dan Brown
ac0f47a4b2 Merge branch 'v0.26' into release 2019-08-06 21:41:06 +01:00
Dan Brown
f417675b1d Prevented normal users from changing own email
To address #1542

Updates to only allow email changes by users with the users-manage role
permission.
2019-08-06 21:29:42 +01:00
Dan Brown
2955f414dd Added iframe JS and data url escaping
Related to #1531
2019-08-06 21:08:24 +01:00
Dan Brown
4de719b325 Prevented potential apache image dir listing
Closes #1545
2019-08-06 20:35:27 +01:00
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
c732970f6e Hardened page content script escaping
Increased range of tests to cover.

Fixes #1531
2019-07-10 20:17:22 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
a602cdf401 Fixed some body card horizontal scroll and column collapse issues
As mentoined in #1441
2019-05-27 13:10:48 +01:00
Dan Brown
5aa741cb60 Prevented tri-layout sidebars being faded on mobile
As mentoined in #1441
2019-05-27 12:56:31 +01:00
Dan Brown
3ad1b42a74 Updated page delete to handle inactive custom homepage correctly
Fixes #1447
2019-05-27 12:40:19 +01:00
Dan Brown
2b7362fa94 Added highlighting to current book-tree item
Related to #1435
2019-05-25 16:52:44 +01:00
Dan Brown
35f35bcba5 Updated custom home views to use tri-layout
Closes #1423
2019-05-25 16:35:27 +01:00
Dan Brown
13c0386e84 Updated string functions to use mulitbyte versions where needed
Fixes #816
2019-05-25 16:15:19 +01:00
Dan Brown
78f5f44460 Updated page navigation click to show content tab on mobile
Fixes #1454
2019-05-25 15:37:49 +01:00
Dan Brown
35e6635379 Fixed chapter description not showing in book exports
Closes #1465
2019-05-25 15:21:02 +01:00
Dan Brown
8ae35f645a Fixed faulty baseUrl rewrites
Fixes #1452
May help #1377
2019-05-19 16:25:05 +01:00
Dan Brown
5470a9e035 Merge branch 'kostefun-patch-1' 2019-05-19 15:43:11 +01:00
Dan Brown
dbfe63ccf6 Fixed missing comma in RU translation array 2019-05-19 15:42:46 +01:00
Dan Brown
114f10d5ca Merge branch 'patch-1' of git://github.com/kostefun/BookStack into kostefun-patch-1 2019-05-19 15:41:48 +01:00
Dan Brown
5226ddd959 Merge pull request #1446 from kostefun/patch-5
Update common.php
2019-05-19 15:41:21 +01:00
Dan Brown
60013f776a Merge branch 'master' into patch-5 2019-05-19 15:40:54 +01:00
Dan Brown
ac0a070fc8 Merge pull request #1445 from kostefun/patch-4
Update entities.php
2019-05-19 15:40:15 +01:00
Dan Brown
b05659d7a3 Merge pull request #1443 from kostefun/patch-3
Update common.php
2019-05-19 15:39:48 +01:00
Dan Brown
3af4648dc3 Merge pull request #1437 from NootoNooto/patch-2
Added Dutch translations for some new texts
2019-05-19 15:36:05 +01:00
Dan Brown
e1e1ea6099 Amended page save button layout to fix z-index issues
- Added a new mobile save button instead of trying to reposition the
original.
- Also recuced the point where the editor top toolbar will collapse to
become x-scrollable.

Fixes #1424
2019-05-19 15:30:58 +01:00
Dan Brown
0c3dc50cd9 Added mobile search bar on search page
Since the header one hides on mobile devices.
Fixes #1450
2019-05-19 15:06:52 +01:00
Dan Brown
0a0ceb382e Doubled image upload display thumb size
Related to #1108
2019-05-19 14:52:17 +01:00
Dan Brown
896f88174a Updated page navigation logic to ignore empty headers
Fixes #1429
2019-05-15 21:02:11 +01:00
Dan Brown
0ee9e5c4db Updated both editors to ignore image paste if text data apparent
Designed to ignore image data when copying from a spreadsheet.
Fixes #987
2019-05-15 20:23:09 +01:00
kostefun
112f73c91c Update settings.php
Ru locale fix
2019-05-14 17:50:23 +07:00
kostefun
b47dc046e0 Update common.php 2019-05-13 17:40:09 +07:00
kostefun
215d84d705 Update entities.php 2019-05-13 17:31:19 +07:00
kostefun
c459d86b58 Update common.php 2019-05-13 17:21:05 +07:00
kostefun
adf0d5fce2 Update settings.php 2019-05-13 17:09:50 +07:00
Nooto
cb355c8aad Modified Bookshelf texts 2019-05-08 23:57:44 +02:00
Nooto
d0e351b942 Added translations for Bookshelves 2019-05-08 23:51:34 +02:00
Nooto
e3d570e928 Update activities.php 2019-05-08 23:25:13 +02:00
Nooto
e00c170d85 Update common.php 2019-05-08 23:24:22 +02:00
Nooto
e430dad38c Added translations for View All, Copy, Reply, etc 2019-05-08 23:05:30 +02:00
52 changed files with 748 additions and 318 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

@@ -760,13 +760,19 @@ class EntityRepo
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//body//*//script');
$scriptElems = $xPath->query('//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')]');
foreach ($badIframes as $badIframe) {
$badIframe->parentNode->removeChild($badIframe);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//body//*/@*[starts-with(name(), \'on\')]');
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
@@ -852,10 +858,13 @@ 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])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
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;
});
}
$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

@@ -146,7 +146,12 @@ class UserController extends Controller
]);
$user = $this->userRepo->getById($id);
$user->fill($request->all());
$user->fill($request->except(['email']));
// Email updates
if (userCan('users-manage') && $request->filled('email')) {
$user->email = $request->get('email');
}
// Role updates
if (userCan('users-manage') && $request->filled('roles')) {

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,10 +123,11 @@ 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));
}
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
// Return normal url path if not specified in config
@@ -134,7 +135,7 @@ function baseUrl($path, $forceAppDomain = false)
return url($path);
}
return $base . '/' . $path;
return $base . '/' . ltrim($path, '/');
}
/**

View File

@@ -1,6 +1,6 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On

12
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -628,6 +628,54 @@ li.checkbox-item, li.task-list-item {
.text-right {
text-align: right; }
@media screen and (min-width: 360px) {
.text-xxs-center {
text-align: center; }
.text-xxs-left {
text-align: left; }
.text-xxs-right {
text-align: right; } }
@media screen and (min-width: 400px) {
.text-xs-center {
text-align: center; }
.text-xs-left {
text-align: left; }
.text-xs-right {
text-align: right; } }
@media screen and (min-width: 600px) {
.text-s-center {
text-align: center; }
.text-s-left {
text-align: left; }
.text-s-right {
text-align: right; } }
@media screen and (min-width: 880px) {
.text-m-center {
text-align: center; }
.text-m-left {
text-align: left; }
.text-m-right {
text-align: right; } }
@media screen and (min-width: 1000px) {
.text-l-center {
text-align: center; }
.text-l-left {
text-align: left; }
.text-l-right {
text-align: right; } }
@media screen and (min-width: 1100px) {
.text-xl-center {
text-align: center; }
.text-xl-left {
text-align: left; }
.text-xl-right {
text-align: right; } }
.text-bigger {
font-size: 1.1em; }
@@ -714,7 +762,7 @@ span.sep {
grid-row-gap: 0; }
@media screen and (max-width: 880px) {
.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) {
grid-template-columns: 1fr; }
@@ -729,7 +777,7 @@ span.sep {
order: 1; } }
@media screen and (max-width: 600px) {
.grid.third {
.grid.third:not(.no-break) {
grid-template-columns: 1fr; } }
@media screen and (max-width: 400px) {
@@ -914,9 +962,6 @@ body.flexbox {
.tri-layout-container .tri-layout-left-contents, .tri-layout-container .tri-layout-right-contents {
padding-left: 16px;
padding-right: 16px; }
.tri-layout-container .tri-layout-right-contents > div, .tri-layout-container .tri-layout-left-contents > div {
opacity: 0.6;
z-index: 0; }
.tri-layout-container .tri-layout-left > *, .tri-layout-container .tri-layout-right > * {
display: none;
pointer-events: none; }
@@ -946,19 +991,18 @@ body.flexbox {
@media screen and (min-width: 1000px) {
.tri-layout-mobile-tabs {
display: none; } }
display: none; }
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms; }
.tri-layout-left-contents > div:hover, .tri-layout-right-contents > div:hover {
opacity: 1; } }
@media screen and (max-width: 880px) {
.tri-layout-container {
margin-left: 0;
margin-right: 0; } }
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms; }
.tri-layout-left-contents > div:hover, .tri-layout-right-contents > div:hover {
opacity: 1; }
/**
* Callouts
*/
@@ -1108,7 +1152,7 @@ body.flexbox {
margin-left: auto;
margin-right: auto;
margin-bottom: 32px;
overflow: auto;
overflow: initial;
min-height: 60vh; }
.content-wrap.card.auto-height {
min-height: 0; }
@@ -1125,7 +1169,7 @@ body.flexbox {
@media screen and (max-width: 600px) {
.content-wrap.card {
padding: 16px 12px; } }
padding: 16px 16px; } }
/**
* Tags
@@ -1961,11 +2005,15 @@ header .search-box {
padding-right: 0; }
.book-tree .sidebar-page-list .entity-list-item {
padding-top: 3px;
padding-bottom: 3px; }
padding-bottom: 3px;
background-clip: content-box;
border-radius: 0 3px 3px 0; }
.book-tree .sidebar-page-list .entity-list-item .content {
padding-top: 6px;
padding-bottom: 6px;
max-width: calc(100% - 20px); }
.book-tree .sidebar-page-list .entity-list-item.selected {
background-color: rgba(0, 0, 0, 0.08); }
.book-tree .sidebar-page-list .entity-list-item.no-hover {
margin-top: -6px;
padding-right: 0; }
@@ -2342,33 +2390,31 @@ ul.pagination {
.page-editor .mce-top-part::before {
box-shadow: none; }
@media screen and (max-width: 880px) {
@media screen and (max-width: 600px) {
.page-edit-toolbar {
overflow-x: scroll;
overflow-y: visible;
z-index: 12; }
overflow-y: visible; }
.page-edit-toolbar .grid.third {
display: block;
white-space: nowrap; }
.page-edit-toolbar .grid.third > div {
display: inline-block; } }
@media screen and (max-width: 880px) {
.page-edit-toolbar #save-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: 16px;
bottom: 12px;
box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.13);
background-color: currentColor; }
.page-edit-toolbar #save-button svg {
fill: #FFF; }
.page-edit-toolbar #save-button span {
display: none; } }
.page-save-mobile-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: 16px;
bottom: 12px;
box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.13);
background-color: currentColor;
text-align: center; }
.page-save-mobile-button svg {
fill: #FFF;
margin-right: 0; }
.draft-notification {
pointer-events: none;

114
public/dist/styles.css vendored
View File

@@ -665,6 +665,54 @@ li.checkbox-item, li.task-list-item {
.text-right {
text-align: right; }
@media screen and (min-width: 360px) {
.text-xxs-center {
text-align: center; }
.text-xxs-left {
text-align: left; }
.text-xxs-right {
text-align: right; } }
@media screen and (min-width: 400px) {
.text-xs-center {
text-align: center; }
.text-xs-left {
text-align: left; }
.text-xs-right {
text-align: right; } }
@media screen and (min-width: 600px) {
.text-s-center {
text-align: center; }
.text-s-left {
text-align: left; }
.text-s-right {
text-align: right; } }
@media screen and (min-width: 880px) {
.text-m-center {
text-align: center; }
.text-m-left {
text-align: left; }
.text-m-right {
text-align: right; } }
@media screen and (min-width: 1000px) {
.text-l-center {
text-align: center; }
.text-l-left {
text-align: left; }
.text-l-right {
text-align: right; } }
@media screen and (min-width: 1100px) {
.text-xl-center {
text-align: center; }
.text-xl-left {
text-align: left; }
.text-xl-right {
text-align: right; } }
.text-bigger {
font-size: 1.1em; }
@@ -815,7 +863,7 @@ span.sep {
grid-row-gap: 0; }
@media screen and (max-width: 880px) {
.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) {
grid-template-columns: 1fr; }
@@ -830,7 +878,7 @@ span.sep {
order: 1; } }
@media screen and (max-width: 600px) {
.grid.third {
.grid.third:not(.no-break) {
grid-template-columns: 1fr; } }
@media screen and (max-width: 400px) {
@@ -1015,9 +1063,6 @@ body.flexbox {
.tri-layout-container .tri-layout-left-contents, .tri-layout-container .tri-layout-right-contents {
padding-left: 16px;
padding-right: 16px; }
.tri-layout-container .tri-layout-right-contents > div, .tri-layout-container .tri-layout-left-contents > div {
opacity: 0.6;
z-index: 0; }
.tri-layout-container .tri-layout-left > *, .tri-layout-container .tri-layout-right > * {
display: none;
pointer-events: none; }
@@ -1047,19 +1092,18 @@ body.flexbox {
@media screen and (min-width: 1000px) {
.tri-layout-mobile-tabs {
display: none; } }
display: none; }
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms; }
.tri-layout-left-contents > div:hover, .tri-layout-right-contents > div:hover {
opacity: 1; } }
@media screen and (max-width: 880px) {
.tri-layout-container {
margin-left: 0;
margin-right: 0; } }
.tri-layout-left-contents > div, .tri-layout-right-contents > div {
opacity: 0.6;
transition: opacity ease-in-out 120ms; }
.tri-layout-left-contents > div:hover, .tri-layout-right-contents > div:hover {
opacity: 1; }
/**
* Callouts
*/
@@ -1209,7 +1253,7 @@ body.flexbox {
margin-left: auto;
margin-right: auto;
margin-bottom: 32px;
overflow: auto;
overflow: initial;
min-height: 60vh; }
.content-wrap.card.auto-height {
min-height: 0; }
@@ -1226,7 +1270,7 @@ body.flexbox {
@media screen and (max-width: 600px) {
.content-wrap.card {
padding: 16px 12px; } }
padding: 16px 16px; } }
/**
* Tags
@@ -3492,11 +3536,15 @@ header .search-box {
padding-right: 0; }
.book-tree .sidebar-page-list .entity-list-item {
padding-top: 3px;
padding-bottom: 3px; }
padding-bottom: 3px;
background-clip: content-box;
border-radius: 0 3px 3px 0; }
.book-tree .sidebar-page-list .entity-list-item .content {
padding-top: 6px;
padding-bottom: 6px;
max-width: calc(100% - 20px); }
.book-tree .sidebar-page-list .entity-list-item.selected {
background-color: rgba(0, 0, 0, 0.08); }
.book-tree .sidebar-page-list .entity-list-item.no-hover {
margin-top: -6px;
padding-right: 0; }
@@ -3873,33 +3921,31 @@ ul.pagination {
.page-editor .mce-top-part::before {
box-shadow: none; }
@media screen and (max-width: 880px) {
@media screen and (max-width: 600px) {
.page-edit-toolbar {
overflow-x: scroll;
overflow-y: visible;
z-index: 12; }
overflow-y: visible; }
.page-edit-toolbar .grid.third {
display: block;
white-space: nowrap; }
.page-edit-toolbar .grid.third > div {
display: inline-block; } }
@media screen and (max-width: 880px) {
.page-edit-toolbar #save-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: 16px;
bottom: 12px;
box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.13);
background-color: currentColor; }
.page-edit-toolbar #save-button svg {
fill: #FFF; }
.page-edit-toolbar #save-button span {
display: none; } }
.page-save-mobile-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: 16px;
bottom: 12px;
box-shadow: 0 2px 2px 1px rgba(0, 0, 0, 0.13);
background-color: currentColor;
text-align: center; }
.page-save-mobile-button svg {
fill: #FFF;
margin-right: 0; }
.draft-notification {
pointer-events: none;

View File

@@ -1,2 +1,3 @@
*
!.gitignore
!.gitignore
!.htaccess

1
public/uploads/.htaccess Executable file
View File

@@ -0,0 +1 @@
Options -Indexes

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) {
@@ -305,12 +308,4 @@ body.flexbox {
margin-left: 0;
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,24 +34,21 @@
}
}
@include smaller-than($m) {
.page-edit-toolbar #save-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: $-m;
bottom: $-s;
box-shadow: $bs-hover;
background-color: currentColor;
svg {
fill: #FFF;
}
span {
display: none;
}
.page-save-mobile-button {
position: fixed;
z-index: 30;
border-radius: 50%;
width: 56px;
height: 56px;
font-size: 24px;
right: $-m;
bottom: $-s;
box-shadow: $bs-hover;
background-color: currentColor;
text-align: center;
svg {
fill: #FFF;
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

@@ -36,6 +36,14 @@ return [
'book_delete_notification' => 'Boek Succesvol Verwijderd',
'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,9 +60,13 @@ return [
'view_profile' => 'Profiel Weergeven',
'edit_profile' => 'Profiel Bewerken',
// Layout tabs
'tab_info' => 'Info',
'tab_content' => 'Inhoud',
/**
* Email Content
*/
'email_action_help' => 'Als je de knop ":actionText" niet werkt, kopieer en plak de onderstaande URL in je web browser:',
'email_rights' => 'Alle rechten voorbehouden',
];
];

View File

@@ -65,16 +65,45 @@ 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',
'x_books' => ':count Boek|:count Boeken',
'books_empty' => 'Er zijn geen boeken aangemaakt',
'books_popular' => 'Populaire Boeken',
'books_recent' => 'Recente Boeken',
'books_new' => 'Nieuwe Boeken',
'books_new' => 'Nieuwe Boeken',
'books_popular_empty' => 'De meest populaire boeken worden hier weergegeven.',
'books_create' => 'Nieuw Boek Aanmaken',
'books_delete' => 'Boek Verwijderen',

View File

@@ -26,6 +26,7 @@ return [
*/
'actions' => 'Действия',
'view' => 'Просмотр',
'view_all' => 'Показать все',
'create' => 'Создание',
'update' => 'Обновление',
'edit' => 'Редактировать',
@@ -39,6 +40,11 @@ return [
'reset' => 'Сбросить',
'remove' => 'Удалить',
'add' => 'Добавить',
// Sort Options
'sort_name' => 'По имени',
'sort_created_at' => 'По дате создания',
'sort_updated_at' => 'По дате обновления',
/**
* Misc
@@ -46,18 +52,23 @@ return [
'deleted_user' => 'Удаленный пользователь',
'no_activity' => 'Нет действий для просмотра',
'no_items' => 'Нет доступных элементов',
'back_to_top' => 'Вернуться наверх',
'back_to_top' => 'Наверх',
'toggle_details' => 'Подробности',
'toggle_thumbnails' => 'Миниатюры',
'details' => 'Детали',
'grid_view' => 'Вид сеткой',
'list_view' => 'Вид списком',
'default' => 'По умолчанию',
/**
* Header
*/
'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
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
// 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,23 +1,19 @@
@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
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@section('left')
@include('common.home-sidebar')
@stop
@include('common.home-sidebar')
</div>
<div>
@include('books.list', ['books' => $books, 'view' => $view])
</div>
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('partials.view-toggle', ['view' => $view, 'type' => 'book'])
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</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="content-wrap card">
<div class="page-content" page-display="{{ $customHomepage->id }}">
@include('pages.page-display', ['page' => $customHomepage])
</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,23 +1,19 @@
@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
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('partials.view-toggle', ['view' => $view, 'type' => 'shelf'])
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@section('left')
@include('common.home-sidebar')
@stop
@include('common.home-sidebar')
</div>
<div>
@include('shelves.list', ['shelves' => $shelves, 'view' => $view])
</div>
@section('right')
<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@include('partials.view-toggle', ['view' => $view, 'type' => 'shelf'])
@include('components.expand-toggle', ['target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
</div>
</div>
@stop

View File

@@ -1,6 +1,7 @@
<input type="text" id="{{ $name }}" name="{{ $name }}"
@if($errors->has($name)) class="text-neg" @endif
@if(isset($placeholder)) placeholder="{{$placeholder}}" @endif
@if(isset($disabled) && $disabled) disabled="disabled" @endif
@if(isset($tabindex)) tabindex="{{$tabindex}}" @endif
@if(isset($model) || old($name)) value="{{ old($name) ? old($name) : $model->$name}}" @endif>
@if($errors->has($name))

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

@@ -19,7 +19,7 @@
<div>
@if($authMethod !== 'ldap' || userCan('users-manage'))
<label for="email">{{ trans('auth.email') }}</label>
@include('form.text', ['name' => 'email'])
@include('form.text', ['name' => 'email', 'disabled' => !userCan('users-manage')])
@endif
</div>
</div>

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

@@ -80,10 +80,66 @@ class PageContentTest extends TestCase
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertDontSee($script);
$pageView->assertSee('abc123abc123');
}
public function test_more_complex_content_script_escaping_scenarios()
{
$checks = [
"<p>Some script</p><script>alert('cat')</script>",
"<div><div><div><div><p>Some script</p><script>alert('cat')</script></div></div></div></div>",
"<p>Some script<script>alert('cat')</script></p>",
"<p>Some script <div><script>alert('cat')</script></div></p>",
"<p>Some script <script><div>alert('cat')</script></div></p>",
"<p>Some script <script><div>alert('cat')</script><script><div>alert('cat')</script></p><script><div>alert('cat')</script>",
];
$this->asEditor();
$page = Page::first();
foreach ($checks as $check) {
$page->html = $check;
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', '<script>');
$pageView->assertElementNotContains('.page-content', '</script>');
}
}
public function test_iframe_js_and_base64_urls_are_removed()
{
$checks = [
'<iframe src="javascript:alert(document.cookie)"></iframe>',
'<iframe SRC=" javascript: alert(document.cookie)"></iframe>',
'<iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
'<iframe src=" data:text/html;base64,PHNjcmlwdD5hbGVydCgnaGVsbG8nKTwvc2NyaXB0Pg==" frameborder="0"></iframe>',
];
$this->asEditor();
$page = Page::first();
foreach ($checks as $check) {
$page->html = $check;
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', '<iframe>');
$pageView->assertElementNotContains('.page-content', '</iframe>');
$pageView->assertElementNotContains('.page-content', 'src=');
$pageView->assertElementNotContains('.page-content', 'javascript:');
$pageView->assertElementNotContains('.page-content', 'data:');
$pageView->assertElementNotContains('.page-content', 'base64');
}
}
public function test_page_inline_on_attributes_removed_by_default()
{
$this->asEditor();
@@ -93,10 +149,36 @@ class PageContentTest extends TestCase
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertDontSee($script);
$pageView->assertSee('<p>Hello</p>');
}
public function test_more_complex_inline_on_attributes_escaping_scenarios()
{
$checks = [
'<p onclick="console.log(\'test\')">Hello</p>',
'<div>Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p>',
'<div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div>',
'<div><div><div><div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div></div></div></div>',
'<div onclick="console.log(\'test\')">Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p><div></div>',
'<a a="<img src=1 onerror=\'alert(1)\'> ',
];
$this->asEditor();
$page = Page::first();
foreach ($checks as $check) {
$page->html = $check;
$page->save();
$pageView = $this->get($page->getUrl());
$pageView->assertStatus(200);
$pageView->assertElementNotContains('.page-content', 'onclick');
}
}
public function test_page_content_scripts_show_when_configured()
{
$this->asEditor();

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

@@ -119,6 +119,43 @@ class RolesTest extends BrowserKitTest
$this->actingAs($this->user)->visit('/')->dontSee($usersLink);
}
public function test_user_cannot_change_email_unless_they_have_manage_users_permission()
{
$userProfileUrl = '/settings/users/' . $this->user->id;
$originalEmail = $this->user->email;
$this->actingAs($this->user);
$this->visit($userProfileUrl)
->assertResponseOk()
->seeElement('input[name=email][disabled]');
$this->put($userProfileUrl, [
'name' => 'my_new_name',
'email' => 'new_email@example.com',
]);
$this->seeInDatabase('users', [
'id' => $this->user->id,
'email' => $originalEmail,
'name' => 'my_new_name',
]);
$this->giveUserPermissions($this->user, ['users-manage']);
$this->visit($userProfileUrl)
->assertResponseOk()
->dontSeeElement('input[name=email][disabled]')
->seeElement('input[name=email]');
$this->put($userProfileUrl, [
'name' => 'my_new_name_2',
'email' => 'new_email@example.com',
]);
$this->seeInDatabase('users', [
'id' => $this->user->id,
'email' => 'new_email@example.com',
'name' => 'my_new_name_2',
]);
}
public function test_user_roles_manage_permission()
{
$this->actingAs($this->user)->visit('/settings/roles')

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]);
}
}

View File

@@ -1 +1 @@
v0.26.1
v0.26.4