Compare commits

..

6 Commits

Author SHA1 Message Date
abijeet
cc275c0b53 Refactored the code for ExportService to use DomDocument.
Fixes #883

Also handling more scenarios.
2019-01-27 15:53:51 +05:30
abijeet
8a2c13729e Merge branch 'master' into fix/video-export 2019-01-05 17:42:20 +05:30
Dan Brown
5f113f3f52 Simplified code a little and renamed dynamicText variable
Just to be a little clearer of what it is.
2018-07-06 12:38:25 +01:00
Dan Brown
27954d6bc6 Added test to cover HTML export re-write
Also altered video regex to be non-greedy to allow mulitple video
matches in a single document
2018-07-06 11:57:11 +01:00
Abijeet
5bee25d651 Fixes a few comments.
Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-07-02 00:39:33 +05:30
Abijeet
0d1db98289 Fixes issues with video tags in PDF, HTML and Text exports.
Closes: #883

Signed-off-by: Abijeet <abijeetpatro@gmail.com>
2018-07-02 00:31:35 +05:30
102 changed files with 728 additions and 8063 deletions

View File

@@ -1,14 +1,11 @@
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
# Environment
APP_ENV=production
APP_DEBUG=false
APP_KEY=SomeRandomString
# Application URL
# Remove the hash below and set a URL if using BookStack behind
# a proxy, if using a third-party authentication option.
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
#APP_URL=https://example.com
# The below url has to be set if using social auth options
# or if you are not using BookStack at the root path of your domain.
# APP_URL=http://bookstack.dev
# Database details
DB_HOST=localhost
@@ -16,16 +13,84 @@ DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# A different prefix is useful when multiple BookStack instances use the same caching server
CACHE_PREFIX=bookstack
# SMTP mail options
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
STORAGE_S3_KEY=false
STORAGE_S3_SECRET=false
STORAGE_S3_REGION=false
STORAGE_S3_BUCKET=false
# Storage URL
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# Disable default services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Set this option to disable LDAPS Certificate Verification
LDAP_TLS_INSECURE=false
# Mail settings
MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# A full list of options can be found in the '.env.example.complete' file.
MAIL_FROM=null
MAIL_FROM_NAME=null

View File

@@ -1,222 +0,0 @@
# Full list of environment variables that can be used with BookStack.
# Selectively copy these to your '.env' file as required.
# Each option is shown with it's default value.
# Do not copy this whole file to use as your '.env' file.
# Application environment
# Can be 'production', 'development', 'testing' or 'demo'
APP_ENV=production
# Enable debug mode
# Shows advanced debug information and errors.
# CAN EXPOSE OTHER VARIABLES, LEAVE DISABLED
APP_DEBUG=false
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# Application URL
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
APP_URL=https://example.com
# Application default language
# The default language choice to show.
# May be overridden by user-preference or visitor browser settings.
APP_LANG=en
# Auto-detect language for public visitors.
# Uses browser-sent headers to infer a language.
# APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
MAIL_FROM=mail@bookstackapp.com
MAIL_FROM_NAME=BookStack
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
CACHE_DRIVER=file
SESSION_DRIVER=file
# Session configuration
SESSION_LIFETIME=120
SESSION_COOKIE_NAME=bookstack_session
SESSION_SECURE_COOKIE=false
# Cache key prefix
# Can be used to prevent conflicts multiple BookStack instances use the same store.
CACHE_PREFIX=bookstack
# Memcached server configuration
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Redis server configuration
# This follows the following format: HOST:PORT:DATABASE
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
# For multiple servers separate with a comma. These will be clustered.
REDIS_SERVERS=127.0.0.1:6379:0
# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
QUEUE_DRIVER=sync
# Storage system to use
# Can be 'local', 'local_secure' or 's3'
STORAGE_TYPE=local
# Amazon S3 storage configuration
STORAGE_S3_KEY=your-s3-key
STORAGE_S3_SECRET=your-s3-secret
STORAGE_S3_BUCKET=s3-bucket-name
STORAGE_S3_REGION=s3-bucket-region
# S3 endpoint to use for storage calls
# Only set this if using a non-Amazon s3-compatible service such as Minio
STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
# Storage URL prefix
# Used as a base for any generated image urls.
# An s3-format URL will be generated if not set.
STORAGE_URL=false
# Authentication method to use
# Can be 'standard' or 'ldap'
AUTH_METHOD=standard
# Social authentication configuration
# All disabled by default.
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
AZURE_APP_ID=false
AZURE_APP_SECRET=false
AZURE_TENANT=false
AZURE_AUTO_REGISTER=false
AZURE_AUTO_CONFIRM_EMAIL=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
DISCORD_AUTO_REGISTER=false
DISCORD_AUTO_CONFIRM_EMAIL=false
FACEBOOK_APP_ID=false
FACEBOOK_APP_SECRET=false
FACEBOOK_AUTO_REGISTER=false
FACEBOOK_AUTO_CONFIRM_EMAIL=false
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GITHUB_AUTO_REGISTER=false
GITHUB_AUTO_CONFIRM_EMAIL=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
GITLAB_AUTO_REGISTER=false
GITLAB_AUTO_CONFIRM_EMAIL=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
GOOGLE_AUTO_REGISTER=false
GOOGLE_AUTO_CONFIRM_EMAIL=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
OKTA_AUTO_REGISTER=false
OKTA_AUTO_CONFIRM_EMAIL=false
SLACK_APP_ID=false
SLACK_APP_SECRET=false
SLACK_AUTO_REGISTER=false
SLACK_AUTO_CONFIRM_EMAIL=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
TWITCH_AUTO_REGISTER=false
TWITCH_AUTO_CONFIRM_EMAIL=false
TWITTER_APP_ID=false
TWITTER_APP_SECRET=false
TWITTER_AUTO_REGISTER=false
TWITTER_AUTO_CONFIRM_EMAIL=false
# LDAP authentication configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
LDAP_TLS_INSECURE=false
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_FOLLOW_REFERRALS=true
# LDAP group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_USER_TO_GROUPS=false
LDAP_GROUP_ATTRIBUTE="memberOf"
LDAP_REMOVE_FROM_GROUPS=false
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable Draw.io integration
DRAWIO=true
# Default item listing view
# Used for public visitors and user's without a preference
# Can be 'list' or 'grid'
APP_VIEWS_BOOKS=list
APP_VIEWS_BOOKSHELVES=grid
# Page revision limit
# Number of page revisions to keep in the system before deleting old revisions.
# If set to 'false' a limit will not be enforced.
REVISION_LIMIT=50
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
# Indicate if robots/crawlers should crawl your instance.
# Can be 'true', 'false' or 'null'.
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null

6
.gitignore vendored
View File

@@ -5,10 +5,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist/*.map
/public/dist
/public/plugins
/public/css/*.map
/public/js/*.map
/public/css
/public/js
/public/bower
/public/build/
/storage/images

View File

@@ -80,40 +80,20 @@ class LdapService
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
return [
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}
/**
* Get a property from an LDAP user response fetch.
* Handles properties potentially being part of an array.
* @param array $userDetails
* @param string $propertyKey
* @param $defaultValue
* @return mixed
*/
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
{
if (isset($userDetails[$propertyKey])) {
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
}
return $defaultValue;
}
/**
* @param Authenticatable $user
* @param string $username
@@ -196,8 +176,8 @@ class LdapService
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
* per handle.
*/
if ($this->config['tls_insecure']) {
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
if($this->config['tls_insecure']) {
$this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);

View File

@@ -190,10 +190,10 @@ class PermissionService
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
/**
@@ -556,39 +556,6 @@ class PermissionService
return $q;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
{
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
$permissionQuery = $this->db->table('joint_permissions')
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
->where(function ($query) use ($userId) {
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
});
}) ;
if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
@@ -645,13 +612,13 @@ class PermissionService
$entities = $this->entityProvider;
$pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);

View File

@@ -84,4 +84,6 @@ class EntityProvider
$type = strtolower($type);
return $this->all()[$type];
}
}
}

View File

@@ -2,9 +2,14 @@
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Uploads\ImageService;
use BookStack\Exceptions\ExportException;
class ExportService
{
protected $contentMatching = [
'video' => ["www.youtube.com", "player.vimeo.com", "www.dailymotion.com"],
'map' => ['maps.google.com']
];
protected $entityRepo;
protected $imageService;
@@ -74,16 +79,17 @@ class ExportService
/**
* Convert a page to a PDF file.
* @param Page $page
* @param bool $isTesting
* @return mixed|string
* @throws \Throwable
*/
public function pageToPdf(Page $page)
public function pageToPdf(Page $page, bool $isTesting = false)
{
$this->entityRepo->renderPage($page);
$html = view('pages/pdf', [
'page' => $page
])->render();
return $this->htmlToPdf($html);
return $this->htmlToPdf($html, $isTesting);
}
/**
@@ -124,12 +130,16 @@ class ExportService
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @param $isTesting
* @return string
* @throws \Exception
*/
protected function htmlToPdf($html)
protected function htmlToPdf($html, $isTesting = false)
{
$containedHtml = $this->containHtml($html);
$containedHtml = $this->containHtml($html, true);
if ($isTesting) {
return $containedHtml;
}
$useWKHTML = config('snappy.pdf.binary') !== false;
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
@@ -143,46 +153,64 @@ class ExportService
/**
* Bundle of the contents of a html file to be self-contained.
* @param $htmlContent
* @param bool $isPDF
* @return mixed|string
* @throws \Exception
* @throws \BookStack\Exceptions\ExportException
*/
protected function containHtml($htmlContent)
protected function containHtml(string $htmlContent, bool $isPDF = false) : string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
$dom = $this->getDOM($htmlContent);
if ($dom === false) {
throw new ExportException(trans('errors.dom_parse_error'));
}
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
$oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
$imageEncoded = $this->imageService->imageUriToBase64($srcString);
if ($imageEncoded === null) {
$imageEncoded = $srcString;
}
$newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
$htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
// replace image src with base64 encoded image strings
$images = $dom->getElementsByTagName('img');
foreach ($images as $img) {
$base64String = $this->imageService->imageUriToBase64($img->getAttribute('src'));
if ($base64String !== null) {
$img->setAttribute('src', $base64String);
$dom->saveHTML($img);
}
}
$linksOutput = [];
preg_match_all("/\<a.*href\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $linksOutput);
// Replace image src with base64 encoded image strings
if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) {
foreach ($linksOutput[0] as $index => $linkMatch) {
$oldLinkString = $linkMatch;
$srcString = $linksOutput[2][$index];
if (strpos(trim($srcString), 'http') !== 0) {
$newSrcString = url($srcString);
$newLinkString = str_replace($srcString, $newSrcString, $oldLinkString);
$htmlContent = str_replace($oldLinkString, $newLinkString, $htmlContent);
}
// replace all relative hrefs.
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (strpos(trim($href), 'http') !== 0) {
$newHref = url($href);
$link->setAttribute('href', $newHref);
$dom->saveHTML($link);
}
}
// Replace any relative links with system domain
return $htmlContent;
// replace all src in video, audio and iframe tags
$xmlDoc = new \DOMXPath($dom);
$srcElements = $xmlDoc->query('//video | //audio | //iframe');
foreach ($srcElements as $element) {
$element = $this->fixRelativeSrc($element);
$dom->saveHTML($element);
if ($isPDF) {
$src = $element->getAttribute('src');
$label = $this->getContentLabel($src);
$div = $dom->createElement('div');
$textNode = $dom->createTextNode($label);
$anchor = $dom->createElement('a');
$anchor->setAttribute('href', $src);
$anchor->textContent = $src;
$div->appendChild($textNode);
$div->appendChild($anchor);
$element->parentNode->replaceChild($div, $element);
}
}
return $dom->saveHTML();
}
/**
@@ -190,11 +218,43 @@ class ExportService
* This method filters any bad looking content to provide a nice final output.
* @param Page $page
* @return mixed
* @throws \BookStack\Exceptions\ExportException
*/
public function pageToPlainText(Page $page)
{
$html = $this->entityRepo->renderPage($page);
$text = strip_tags($html);
$dom = $this->getDom($html);
if ($dom === false) {
throw new ExportException(trans('errors.dom_parse_error'));
}
// handle anchor tags.
$links = $dom->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (strpos(trim($href), 'http') !== 0) {
$newHref = url($href);
$link->setAttribute('href', $newHref);
}
$link->textContent = trim($link->textContent . " ($href)");
$dom->saveHTML();
}
$xmlDoc = new \DOMXPath($dom);
$srcElements = $xmlDoc->query('//video | //audio | //iframe | //img');
foreach ($srcElements as $element) {
$element = $this->fixRelativeSrc($element);
$fixedSrc = $element->getAttribute('src');
$label = $this->getContentLabel($fixedSrc);
$finalLabel = "\n\n$label $fixedSrc\n\n";
$textNode = $dom->createTextNode($finalLabel);
$element->parentNode->replaceChild($textNode, $element);
}
$text = strip_tags($dom->saveHTML());
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.
@@ -238,4 +298,37 @@ class ExportService
}
return $text;
}
protected function getDom(string $htmlContent) : \DOMDocument
{
// See - https://stackoverflow.com/a/17559716/903324
$dom = new \DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($htmlContent);
libxml_clear_errors();
return $dom;
}
protected function fixRelativeSrc(\DOMElement $element): \DOMElement
{
$src = $element->getAttribute('src');
if (strpos(trim($src), 'http') !== 0) {
$newSrc = 'https:' . $src;
$element->setAttribute('src', $newSrc);
}
return $element;
}
protected function getContentLabel(string $src) : string
{
foreach ($this->contentMatching as $key => $possibleValues) {
foreach ($possibleValues as $value) {
if (strpos($src, $value)) {
return trans("entities.$key");
}
}
}
return trans('entities.embedded_content');
}
}

View File

@@ -599,47 +599,23 @@ class EntityRepo
}
/**
* Render the page for viewing
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @param bool $blankIncludes
* @return string
* @param bool $ignorePermissions
* @return mixed|string
*/
public function renderPage(Page $page, bool $blankIncludes = false) : string
public function renderPage(Page $page, $ignorePermissions = false)
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
if ($blankIncludes) {
$content = $this->blankPageIncludes($content);
} else {
$content = $this->parsePageIncludes($content);
}
return $content;
}
/**
* Remove any page include tags within the given HTML.
* @param string $html
* @return string
*/
protected function blankPageIncludes(string $html) : string
{
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
}
/**
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
* @param string $html
* @return mixed|string
*/
protected function parsePageIncludes(string $html) : string
{
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) {
return $content;
}
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
@@ -649,14 +625,14 @@ class EntityRepo
continue;
}
$matchedPage = $this->getById('page', $pageId);
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
if ($matchedPage === null) {
$html = str_replace($matches[0][$index], '', $html);
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
continue;
}
@@ -664,7 +640,7 @@ class EntityRepo
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$html = str_replace($matches[0][$index], '', $html);
$content = str_replace($matches[0][$index], '', $content);
continue;
}
$innerContent = '';
@@ -676,22 +652,25 @@ class EntityRepo
$innerContent .= $doc->saveHTML($childNode);
}
}
$html = str_replace($matches[0][$index], trim($innerContent), $html);
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
return $html;
return $content;
}
/**
* Escape script tags within HTML content.
* @param string $html
* @return string
* @return mixed
*/
protected function escapeScripts(string $html) : string
protected function escapeScripts(string $html)
{
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
return $html;
}
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);

View File

@@ -194,9 +194,9 @@ class PageRepo extends EntityRepo
* @param \BookStack\Entities\Page $page
* @return string
*/
protected function pageToPlainText(Page $page) : string
public function pageToPlainText(Page $page)
{
$html = $this->renderPage($page, true);
$html = $this->renderPage($page);
return strip_tags($html);
}
@@ -505,4 +505,4 @@ class PageRepo extends EntityRepo
return $this->publishPageDraft($copyPage, $pageData);
}
}
}

View File

@@ -0,0 +1,6 @@
<?php namespace BookStack\Exceptions;
class ExportException extends PrettyException
{
}

View File

@@ -2,6 +2,4 @@
use Exception;
class HttpFetchException extends Exception
{
}
class HttpFetchException extends Exception {}

View File

@@ -1,5 +1,3 @@
<?php namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException
{
}
class UserUpdateException extends NotifyException {}

View File

@@ -161,7 +161,6 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters/move', [
'chapter' => $chapter,
'book' => $chapter->book
@@ -180,7 +179,6 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {

View File

@@ -119,7 +119,7 @@ class ImageController extends Controller
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'image_extension|mimes:jpeg,png,gif,bmp,webp,tiff'
'file' => 'is_image'
]);
if (!$this->imageRepo->isValidType($type)) {
@@ -135,6 +135,7 @@ class ImageController extends Controller
return response($e->getMessage(), 500);
}
return response()->json($image);
}

View File

@@ -495,13 +495,15 @@ class PageController extends Controller
* https://github.com/barryvdh/laravel-dompdf
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $pageSlug)
public function exportPdf($bookSlug, $pageSlug, Request $request)
{
$isTesting = $request->query('isTesting');
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$page->html = $this->pageRepo->renderPage($page);
$pdfContent = $this->exportService->pageToPdf($page);
$pdfContent = $this->exportService->pageToPdf($page, !empty($isTesting));
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
}
@@ -586,7 +588,6 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
return view('pages/move', [
'book' => $page->book,
'page' => $page
@@ -605,7 +606,6 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -643,7 +643,7 @@ class PageController extends Controller
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission('page-update', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
'book' => $page->book,
@@ -662,7 +662,7 @@ class PageController extends Controller
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission('page-update', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {

View File

@@ -7,40 +7,8 @@ use Illuminate\Http\Request;
class Localization
{
/**
* Array of right-to-left locales
* @var array
*/
protected $rtlLocales = ['ar'];
/**
* Map of BookStack locale names to best-estimate system locale names.
* @var array
*/
protected $localeMap = [
'ar' => 'ar',
'de' => 'de_DE',
'de_informal' => 'de_DE',
'en' => 'en_GB',
'es' => 'es_ES',
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'it' => 'it_IT',
'ja' => 'ja',
'kr' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt_BR' => 'pt_BR',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
'sv' => 'sv_SE',
'uk' => 'uk_UA',
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
];
/**
* Handle an incoming request.
*
@@ -51,7 +19,6 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
config()->set('app.default_locale', $defaultLang);
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
@@ -66,7 +33,6 @@ class Localization
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
return $next($request);
}
@@ -87,18 +53,4 @@ class Localization
}
return $default;
}
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
* @param string $locale
*/
protected function setSystemDateLocale(string $locale)
{
$systemLocale = $this->localeMap[$locale] ?? $locale;
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
}
}
}

View File

@@ -31,4 +31,5 @@ class MailNotification extends Notification implements ShouldQueue
'text' => 'vendor.notifications.email-plain'
]);
}
}
}

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Notifications;
class ResetPassword extends MailNotification
{
/**

View File

@@ -8,7 +8,6 @@ use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@@ -23,12 +22,11 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo icon($expression); ?>";

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
@@ -28,4 +29,4 @@ class TranslationServiceProvider extends \Illuminate\Translation\TranslationServ
return $trans;
});
}
}
}

View File

@@ -41,7 +41,6 @@ class SettingService
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
@@ -69,4 +70,5 @@ class Translator extends \Illuminate\Translation\Translator
{
return $this->baseLocaleMap[$locale] ?? null;
}
}
}

View File

@@ -30,4 +30,5 @@ class HttpFetcher
return $data;
}
}
}

View File

@@ -1,7 +1,5 @@
<?php
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Ownable;
/**
@@ -52,34 +50,21 @@ function signedInUser()
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
* that particular item.
* @param string $permission
* @param $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan(string $permission, Ownable $ownable = null)
function userCan($permission, Ownable $ownable = null)
{
if ($ownable === null) {
return user() && user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(PermissionService::class);
$permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
/**
* Check if the current user has the given permission
* on any item in the system.
* @param string $permission
* @param string|null $entityClass
* @return bool
*/
function userCanOnAny(string $permission, string $entityClass = null)
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
}
/**
* Helper to access system settings.
* @param $key

View File

@@ -16,7 +16,7 @@
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
"laravel/socialite": "3.0.x-dev",
"laravel/socialite": "^3.0",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",

93
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "0946a07729a7a1bfef9bac185a870afd",
"content-hash": "06219a5c2419ca23ec2924eb31f4ed16",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.86.2",
"version": "3.82.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2019-01-18T21:10:44+00:00"
"time": "2018-12-21T22:21:50+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
@@ -1457,16 +1457,16 @@
},
{
"name": "laravel/socialite",
"version": "3.0.x-dev",
"version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
"url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481",
"shasum": ""
},
"require": {
@@ -1516,7 +1516,7 @@
"laravel",
"oauth"
],
"time": "2018-12-21T14:06:32+00:00"
"time": "2018-10-18T03:39:04+00:00"
},
{
"name": "league/flysystem",
@@ -1891,16 +1891,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.36.2",
"version": "1.36.1",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"shasum": ""
},
"require": {
@@ -1945,7 +1945,7 @@
"datetime",
"time"
],
"time": "2018-12-28T10:07:33+00:00"
"time": "2018-11-22T18:23:02+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2555,20 +2555,20 @@
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.4",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"shasum": ""
},
"require": {
"laravel/socialite": "~3.0|~4.0",
"laravel/socialite": "~3.0",
"php": "^5.6 || ^7.0"
},
"require-dev": {
@@ -2585,7 +2585,8 @@
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/"
"SocialiteProviders\\Manager\\": "src/",
"SocialiteProviders\\Manager\\Test\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2596,14 +2597,10 @@
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2019-01-16T07:58:54+00:00"
"time": "2017-11-20T08:42:57+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@@ -3667,16 +3664,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.5.2",
"version": "v2.5.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"shasum": ""
},
"require": {
@@ -3713,7 +3710,7 @@
"env",
"environment"
],
"time": "2018-10-30T17:29:25+00:00"
"time": "2018-07-29T20:33:41+00:00"
}
],
"packages-dev": [
@@ -4426,23 +4423,23 @@
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.8",
"version": "5.2.7",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4"
"reference": "8560d4314577199ba51bf2032f02cd1315587c23"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.2.20",
"friendsofphp/php-cs-fixer": "^2.1",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
@@ -4488,7 +4485,7 @@
"json",
"schema"
],
"time": "2019-01-14T23:55:14+00:00"
"time": "2018-02-14T22:26:30+00:00"
},
{
"name": "laravel/browser-kit-testing",
@@ -6268,21 +6265,20 @@
},
{
"name": "webmozart/assert",
"version": "1.4.0",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@@ -6315,14 +6311,12 @@
"check",
"validate"
],
"time": "2018-12-25T11:19:39+00:00"
"time": "2018-01-29T19:49:41+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"laravel/socialite": 20
},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -6332,8 +6326,7 @@
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*",
"ext-curl": "*"
"ext-gd": "*"
},
"platform-dev": [],
"platform-overrides": {

View File

@@ -22,8 +22,7 @@ return [
// Set the default view type for various lists. Can be overridden by user preferences.
// These will be used for public viewers and users that have not set a preference.
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list'),
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
'books' => env('APP_VIEWS_BOOKS', 'list')
],
// The number of revisions to keep in the database.

View File

@@ -8,39 +8,23 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
// REDIS
// Split out configuration into an array
// REDIS - Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServerKeys = ['host', 'port', 'database'];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [];
$cluster = count($redisServers) > 1;
if ($cluster) {
$redisConfig['clusters'] = ['default' => []];
}
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
$serverConfig = [];
$configIndex = 0;
foreach ($redisDefaults as $configKey => $configDefault) {
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
$configIndex++;
}
if ($cluster) {
$redisConfig['clusters']['default'][] = $serverConfig;
} else {
$redisConfig['default'] = $serverConfig;
}
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
}
}
// MYSQL
// Split out port from host if set
// MYSQL - Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);

View File

@@ -49,8 +49,6 @@ return [
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
'region' => env('STORAGE_S3_REGION', 'your-region'),
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
'rackspace' => [

View File

@@ -141,7 +141,6 @@ return [
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),

View File

@@ -14,6 +14,7 @@ return [
// Options: file, cookie, database, redis, memcached, array
'driver' => env('SESSION_DRIVER', 'file'),
// Session lifetime, in minutes
'lifetime' => env('SESSION_LIFETIME', 120),

67
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
header {
display: none; }
body {
font-size: 12px; }
.faded-small {
display: none; }
.page-content {
margin: 0 auto; }
.flex-fill {
display: block; }
.flex.sidebar + .flex.content {
border-left: none; }
.print-hidden {
display: none; }
.print-full-width {
width: 100%;
float: none;
display: block; }
h2 {
font-size: 2em;
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em; }
.comments-container {
display: none; }

4298
public/dist/styles.css vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
!function(){"use strict";var a=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return a(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),i=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},u=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},s=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},c=function(t){var e=parseInt(r.getItem(u(t)+"time"),10)||0;return!((new Date).getTime()-e>i(t.settings.autosave_retention,"20m")&&(f(t,!1),1))},f=function(t,e){var n=u(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},l=function(t){var e=u(t);!s(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},m=function(t){var e=u(t);c(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},v=function(t,e){var n=i(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||l(t)},n),e.set(!0))},d=function(t){t.undoManager.transact(function(){m(t),f(t)}),t.focus()};function g(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var y=tinymce.util.Tools.resolve("tinymce.EditorManager");y._beforeUnloadHandler=function(){var e;return o.each(y.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var p=function(n,r){return function(t){var e=t.control;e.disabled(!c(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!c(n))}),v(n,r)}};t.add("autosave",function(t){var e,n,r,o=a(!1);return window.onbeforeunload=y._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){d(e)},onPostRender:p(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){d(e)},onPostRender:p(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&m(t)}),{hasDraft:g(c,r=t),storeDraft:g(l,r),restoreDraft:g(m,r),removeDraft:g(f,r),isEmpty:g(s,r)}})}();
!function(){"use strict";var n=function(t){var e=t,r=function(){return e};return{get:r,set:function(t){e=t},clone:function(){return n(r())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),r=function(t){return t.fire("RestoreDraft")},i=function(t){return t.fire("StoreDraft")},s=function(t){return t.fire("RemoveDraft")},e=function(t,e){return((t=/^(\d+)([ms]?)$/.exec(""+(t||e)))[2]?{s:1e3,m:6e4}[t[2]]:1)*parseInt(t,10)},u=function(t){return t.getParam("autosave_ask_before_unload",!0)},f=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t){return e(t.settings.autosave_interval,"30s")},l=function(t){return e(t.settings.autosave_retention,"20m")},m=function(t,e){var r=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+r+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+r+">|<br>$","i").test(e)},v=function(t){var e=parseInt(a.getItem(f(t)+"time"),10)||0;return!((new Date).getTime()-e>l(t)&&(d(t,!1),1))},d=function(t,e){var r=f(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&s(t)},D=function(t){var e=f(t);!m(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),i(t))},g=function(t){var e=f(t);v(t)&&(t.setContent(a.getItem(e+"draft"),{format:"raw"}),r(t))},y={isEmpty:m,hasDraft:v,removeDraft:d,storeDraft:D,restoreDraft:g,startStoreDraft:function(t,e){var r=c(t);e.get()||(setInterval(function(){t.removed||D(t)},r),e.set(!0))},restoreLastDraft:function(t){t.undoManager.transact(function(){g(t),d(t)}),t.focus()}},p=function(e,r){return function(){var t=Array.prototype.slice.call(arguments);return e.apply(null,[r].concat(t))}},h=function(t){return{hasDraft:p(y.hasDraft,t),storeDraft:p(y.storeDraft,t),restoreDraft:p(y.restoreDraft,t),removeDraft:p(y.removeDraft,t),isEmpty:p(y.isEmpty,t)}},_=tinymce.util.Tools.resolve("tinymce.EditorManager");_._beforeUnloadHandler=function(){var e;return o.each(_.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var b=function(t){window.onbeforeunload=_._beforeUnloadHandler},I=function(r,n){return function(t){var e=t.control;e.disabled(!y.hasDraft(r)),r.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!y.hasDraft(r))}),y.startStoreDraft(r,n)}},w=function(t,e){t.addButton("restoredraft",{title:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e)}),t.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e),context:"file"})};t.add("autosave",function(t){var e=n(!1);return b(t),w(t,e),h(t)})}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -87,7 +87,6 @@
<glyph unicode="&#xe906;" glyph-name="reload" d="M889.68 793.68c-93.608 102.216-228.154 166.32-377.68 166.32-282.77 0-512-229.23-512-512h96c0 229.75 186.25 416 416 416 123.020 0 233.542-53.418 309.696-138.306l-149.696-149.694h352v352l-134.32-134.32zM928 448c0-229.75-186.25-416-416-416-123.020 0-233.542 53.418-309.694 138.306l149.694 149.694h-352v-352l134.32 134.32c93.608-102.216 228.154-166.32 377.68-166.32 282.77 0 512 229.23 512 512h-96z" />
<glyph unicode="&#xe907;" glyph-name="translate" d="M553.6 304l-118.4 118.4c80 89.6 137.6 195.2 172.8 304h137.6v92.8h-326.4v92.8h-92.8v-92.8h-326.4v-92.8h518.4c-32-89.6-80-176-147.2-249.6-44.8 48-80 99.2-108.8 156.8h-92.8c35.2-76.8 80-147.2 137.6-211.2l-236.8-233.6 67.2-67.2 233.6 233.6 144-144c3.2 0 38.4 92.8 38.4 92.8zM816 540.8h-92.8l-208-560h92.8l51.2 140.8h220.8l51.2-140.8h92.8l-208 560zM691.2 214.4l76.8 201.6 76.8-201.6h-153.6z" />
<glyph unicode="&#xe908;" glyph-name="drag" d="M576 896h128v-128h-128v128zM576 640h128v-128h-128v128zM320 640h128v-128h-128v128zM576 384h128v-128h-128v128zM320 384h128v-128h-128v128zM320 128h128v-128h-128v128zM576 128h128v-128h-128v128zM320 896h128v-128h-128v128z" />
<glyph unicode="&#xe909;" glyph-name="format-painter" d="M768 746.667v42.667c0 23.467-19.2 42.667-42.667 42.667h-512c-23.467 0-42.667-19.2-42.667-42.667v-170.667c0-23.467 19.2-42.667 42.667-42.667h512c23.467 0 42.667 19.2 42.667 42.667v42.667h42.667v-170.667h-426.667v-384c0-23.467 19.2-42.667 42.667-42.667h85.333c23.467 0 42.667 19.2 42.667 42.667v298.667h341.333v341.333h-128z" />
<glyph unicode="&#xe90b;" glyph-name="home" d="M1024 369.556l-512 397.426-512-397.428v162.038l512 397.426 512-397.428zM896 384v-384h-256v256h-256v-256h-256v384l384 288z" />
<glyph unicode="&#xe911;" glyph-name="books" d="M576.234 670.73l242.712 81.432 203.584-606.784-242.712-81.432zM0 64h256v704h-256v-704zM64 640h128v-64h-128v64zM320 64h256v704h-256v-704zM384 640h128v-64h-128v64z" />
<glyph unicode="&#xe914;" glyph-name="upload" d="M839.432 760.57c27.492-27.492 50.554-78.672 55.552-120.57h-318.984v318.984c41.898-4.998 93.076-28.060 120.568-55.552l142.864-142.862zM512 576v384h-368c-44 0-80-36-80-80v-864c0-44 36-80 80-80h672c44 0 80 36 80 80v560h-384zM576 192v-192h-192v192h-160l256 256 256-256h-160z" />

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@ A platform for storing and organising information and documentation. General inf
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
* [Documentation](https://www.bookstackapp.com/docs)
* [Demo Instance](https://demo.bookstackapp.com)
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
* *Username: `admin@example.com`*
* *Password: `password`*
* [BookStack Blog](https://www.bookstackapp.com/blog)
## Project Definition
@@ -18,30 +19,7 @@ BookStack is an opinionated wiki system that provides a pleasant and simple out
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
## Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
- *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
- **Platform REST API**
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
- **Editor Alignment & Review**
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
- **Permission System Review**
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
## Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
In regards to development philosophy, BookStack has a relaxed, open & positive approach. Put simply, At the end of the day this is free software developed and maintained by people donating their own free time.
## Development & Testing
@@ -107,15 +85,15 @@ PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr
### Pull Requests
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
## Website, Docs & Blog
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## License
@@ -139,6 +117,7 @@ These are the great open-source projects used to help build BookStack:
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
* [Dompdf](https://github.com/barryvdh/laravel-dompdf)

View File

@@ -8,11 +8,7 @@ class MarkdownEditor {
constructor(elem) {
this.elem = elem;
const pageEditor = document.getElementById('page-editor');
this.pageId = pageEditor.getAttribute('page-id');
this.textDirection = pageEditor.getAttribute('text-direction');
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
@@ -102,9 +98,7 @@ class MarkdownEditor {
}
codeMirrorSetup() {
const cm = this.cm;
const context = this;
let cm = this.cm;
// Text direction
// cm.setOption('direction', this.textDirection);
cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
@@ -272,18 +266,17 @@ class MarkdownEditor {
}
// Insert image into markdown
const id = "image-" + Math.random().toString(16).slice(2);
const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
const selectedText = cm.getSelection();
const placeHolderText = `![${selectedText}](${placeholderImage})`;
const cursor = cm.getCursor();
let id = "image-" + Math.random().toString(16).slice(2);
let placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
let selectedText = cm.getSelection();
let placeHolderText = `![${selectedText}](${placeholderImage})`;
let cursor = cm.getCursor();
cm.replaceSelection(placeHolderText);
cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
const remoteFilename = "image-" + Date.now() + "." + ext;
const formData = new FormData();
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', context.pageId);
window.$http.post('/images/gallery/upload', formData).then(resp => {
const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
@@ -309,7 +302,7 @@ class MarkdownEditor {
}
actionInsertImage() {
const cursorPos = this.cm.getCursor('from');
let cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
let selectedText = this.cm.getSelection();
let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
@@ -320,7 +313,7 @@ class MarkdownEditor {
}
actionShowImageManager() {
const cursorPos = this.cm.getCursor('from');
let cursorPos = this.cm.getCursor('from');
window.ImageManager.show(image => {
this.insertDrawing(image, cursorPos);
}, 'drawio');
@@ -328,7 +321,7 @@ class MarkdownEditor {
// Show the popup link selector and insert a link when finished
actionShowLinkSelector() {
const cursorPos = this.cm.getCursor('from');
let cursorPos = this.cm.getCursor('from');
window.EntitySelectorPopup.show(entity => {
let selectedText = this.cm.getSelection() || entity.name;
let newText = `[${selectedText}](${entity.link})`;
@@ -364,7 +357,7 @@ class MarkdownEditor {
}
insertDrawing(image, originalCursor) {
const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
let newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
this.cm.focus();
this.cm.replaceSelection(newText);
this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
@@ -372,13 +365,9 @@ class MarkdownEditor {
// Show draw.io if enabled and handle save.
actionEditDrawing(imgContainer) {
const drawingDisabled = document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true';
if (drawingDisabled) {
return;
}
const cursorPos = this.cm.getCursor('from');
const drawingId = imgContainer.getAttribute('drawio-diagram');
if (document.querySelector('[drawio-enabled]').getAttribute('drawio-enabled') !== 'true') return;
let cursorPos = this.cm.getCursor('from');
let drawingId = imgContainer.getAttribute('drawio-diagram');
DrawIO.show(() => {
return window.$http.get(window.baseUrl(`/images/base64/${drawingId}`)).then(resp => {

View File

@@ -123,21 +123,20 @@ class PageDisplay {
setupStickySidebar() {
// Make the sidebar stick in view on scroll
const $window = $(window);
const $sidebar = $("#sidebar .scroll-body");
const $sidebarContainer = $sidebar.parent();
const sidebarHeight = $sidebar.height() + 32;
let $window = $(window);
let $sidebar = $("#sidebar .scroll-body");
let $bookTreeParent = $sidebar.parent();
// Check the page is scrollable and the content is taller than the tree
const pageScrollable = ($(document).height() > ($window.height() + 40)) && (sidebarHeight < $('.page-content').height());
let pageScrollable = ($(document).height() > ($window.height() + 40)) && ($sidebar.height() < $('.page-content').height());
// Get current tree's width and header height
const headerHeight = $("#header").height() + $(".toolbar").height();
let headerHeight = $("#header").height() + $(".toolbar").height();
let isFixed = $window.scrollTop() > headerHeight;
// Fix the tree as a sidebar
function stickTree() {
$sidebar.width($sidebarContainer.width() + 15);
$sidebar.width($bookTreeParent.width() + 15);
$sidebar.addClass("fixed");
isFixed = true;
}

View File

@@ -4,24 +4,22 @@ import DrawIO from "../services/drawio";
/**
* Handle pasting images from clipboard.
* @param {ClipboardEvent} event
* @param {WysiwygEditor} wysiwygComponent
* @param editor
*/
function editorPaste(event, editor, wysiwygComponent) {
function editorPaste(event, editor) {
if (!event.clipboardData || !event.clipboardData.items) return;
let items = event.clipboardData.items;
for (let clipboardItem of event.clipboardData.items) {
if (clipboardItem.type.indexOf("image") === -1) continue;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") === -1) continue;
event.preventDefault();
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');
const file = clipboardItem.getAsFile();
let id = "image-" + Math.random().toString(16).slice(2);
let loadingImage = window.baseUrl('/loading.gif');
let file = items[i].getAsFile();
setTimeout(() => {
editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
uploadImageFile(file, wysiwygComponent).then(resp => {
uploadImageFile(file).then(resp => {
editor.dom.setAttrib(id, 'src', resp.thumbs.display);
}).catch(err => {
editor.dom.remove(id);
@@ -35,12 +33,9 @@ function editorPaste(event, editor, wysiwygComponent) {
/**
* Upload an image file to the server
* @param {File} file
* @param {WysiwygEditor} wysiwygComponent
*/
async function uploadImageFile(file, wysiwygComponent) {
if (file === null || file.type.indexOf('image') !== 0) {
throw new Error(`Not an image file`);
}
function uploadImageFile(file) {
if (file === null || file.type.indexOf('image') !== 0) return Promise.reject(`Not an image file`);
let ext = 'png';
if (file.name) {
@@ -48,13 +43,11 @@ async function uploadImageFile(file, wysiwygComponent) {
if (fileNameMatches.length > 1) ext = fileNameMatches[1];
}
const remoteFilename = "image-" + Date.now() + "." + ext;
const formData = new FormData();
let remoteFilename = "image-" + Date.now() + "." + ext;
let formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', wysiwygComponent.pageId);
const resp = await window.$http.post(window.baseUrl('/images/gallery/upload'), formData);
return resp.data;
return window.$http.post(window.baseUrl('/images/gallery/upload'), formData).then(resp => (resp.data));
}
function registerEditorShortcuts(editor) {
@@ -377,10 +370,7 @@ class WysiwygEditor {
constructor(elem) {
this.elem = elem;
const pageEditor = document.getElementById('page-editor');
this.pageId = pageEditor.getAttribute('page-id');
this.textDirection = pageEditor.getAttribute('text-direction');
this.textDirection = document.getElementById('page-editor').getAttribute('text-direction');
this.plugins = "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists codeeditor media";
this.loadPlugins();
@@ -407,9 +397,6 @@ class WysiwygEditor {
}
getTinyMceConfig() {
const context = this;
return {
selector: '#html-editor',
content_css: [
@@ -599,7 +586,7 @@ class WysiwygEditor {
});
// Paste image-uploads
editor.on('paste', event => editorPaste(event, editor, context));
editor.on('paste', event => editorPaste(event, editor));
}
};
}

View File

@@ -8,7 +8,6 @@ import 'codemirror/mode/diff/diff';
import 'codemirror/mode/go/go';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/lua/lua';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/nginx/nginx';
import 'codemirror/mode/php/php';
@@ -39,13 +38,12 @@ const modeMap = {
javascript: 'javascript',
json: {name: 'javascript', json: true},
js: 'javascript',
lua: 'lua',
php: 'php',
md: 'markdown',
mdown: 'markdown',
markdown: 'markdown',
nginx: 'nginx',
powershell: 'powershell',
php: 'php',
py: 'python',
python: 'python',
ruby: 'ruby',

View File

@@ -16,7 +16,6 @@ function mounted() {
addRemoveLinks: true,
dictRemoveFile: trans('components.image_upload_remove'),
timeout: Number(window.uploadTimeout) || 60000,
maxFilesize: Number(window.uploadLimit) || 256,
url: function() {
return _this.uploadUrl;
},

View File

@@ -210,6 +210,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
.image-manager-sidebar {
width: 300px;
margin-left: 1px;
overflow-y: auto;
overflow-x: hidden;
border-left: 1px solid #DDD;
@@ -523,8 +524,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
font-size: 12px;
line-height: 1.2;
top: 88px;
left: -12px;
width: 160px;
left: -26px;
width: 148px;
background: $negative;
padding: $-xs;
color: white;
@@ -534,7 +535,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
content: '';
position: absolute;
top: -6px;
left: 44px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;

View File

@@ -51,22 +51,15 @@
margin: $-xs $-s $-xs 0;
}
.align-right {
text-align: right !important;
float: right !important;
}
img.align-right, table.align-right {
float: right !important;
text-align: right;
margin: $-xs 0 $-xs $-s;
}
.align-center {
text-align: center;
}
img.align-center {
display: block;
}
img.align-center, table.align-center {
margin-left: auto;
margin-right: auto;
}
img {
max-width: 100%;
height:auto;

View File

@@ -53,9 +53,4 @@
// Fix to prevent 'No color' option from not being clickable.
.mce-colorbtn-trans {
overflow: hidden;
}
// Fix to prevent CodeMirror focus events throwing TinyMCE cursor position.
.mce-content-body .CodeMirrorContainer > .CodeMirror {
pointer-events: none;
}

View File

@@ -16,14 +16,6 @@ body {
margin: 0 auto;
}
.flex-fill {
display: block;
}
.flex.sidebar + .flex.content {
border-left: none;
}
.print-hidden {
display: none;
}
@@ -39,8 +31,4 @@ h2 {
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em;
}
.comments-container {
display: none;
}

View File

@@ -60,39 +60,6 @@ return [
'search_created_after' => 'Erstellt nach',
'search_set_date' => 'Datum auswählen',
'search_update' => 'Suche aktualisieren',
/*
* Shelves
*/
'shelf' => 'Regal',
'shelves' => 'Regale',
'shelves_long' => 'Bücherregal',
'shelves_empty' => 'Es wurden noch keine Regale angelegt',
'shelves_create' => 'Erzeuge ein Regal',
'shelves_popular' => 'Beliebte Regale',
'shelves_new' => 'Kürzlich erstellte Regale',
'shelves_popular_empty' => 'Die beliebtesten Regale werden hier angezeigt.',
'shelves_new_empty' => 'Die neusten Regale werden hier angezeigt.',
'shelves_save' => 'Regal speichern',
'shelves_books' => 'Bücher in diesem Regal',
'shelves_add_books' => 'Buch zu diesem Regal hinzufügen',
'shelves_drag_books' => 'Bücher hier hin ziehen um sie dem Regal hinzuzufügen',
'shelves_empty_contents' => 'Diesem Regal sind keine Bücher zugewiesen',
'shelves_edit_and_assign' => 'Regal bearbeiten um Bücher hinzuzufügen',
'shelves_edit_named' => 'Bücherregal :name bearbeiten',
'shelves_edit' => 'Bücherregal bearbeiten',
'shelves_delete' => 'Bücherregal löschen',
'shelves_delete_named' => 'Bücherregal :name löschen',
'shelves_delete_explain' => "Sie sind im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
'shelves_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Bücherregal löschen wollen?',
'shelves_permissions' => 'Regal-Berechtigungen',
'shelves_permissions_updated' => 'Regal-Berechtigungen aktualisiert',
'shelves_permissions_active' => 'Regal-Berechtigungen aktiv',
'shelves_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
'shelves_copy_permissions' => 'Berechtigungen kopieren',
'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfen Sie vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
'shelves_copy_permission_success' => 'Regal-Berechtigungen wurden zu :count Büchern kopiert',
/**
* Books
*/

View File

@@ -9,13 +9,6 @@ return [
'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',
/**
* Shelves
*/
'shelves_delete_explain' => "Du bist im Begriff das Bücherregal mit dem Namen ':name' zu löschen. Enthaltene Bücher werden nicht gelöscht.",
'shelves_delete_confirmation' => 'Bist du sicher, dass du dieses Bücherregal löschen willst?',
'shelves_copy_permissions_explain' => 'Hiermit werden die Berechtigungen des aktuellen Regals auf alle enthaltenen Bücher übertragen. Überprüfe vor der Aktivierung, ob alle Berechtigungsänderungen am aktuellen Regal gespeichert wurden.',
/**
* Books
*/

View File

@@ -289,5 +289,11 @@ return [
// Revision
'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
'revision_delete_success' => 'Revision deleted',
'revision_cannot_delete_latest' => 'Cannot delete the latest revision.'
'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
// PDF / Text Embeds
'video' => 'Video: ',
'map' => 'Map: ',
'embedded_content' => 'Embedded Content: '
];

View File

@@ -81,4 +81,7 @@ return [
'app_down' => ':appName is down right now',
'back_soon' => 'It will be back up soon.',
// Export errors
'dom_parse_error' => 'There was an error while exporting the page. This maybe caused due to the HTML structure of the page.'
];

View File

@@ -33,7 +33,6 @@ return [
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'image_extension' => 'The :attribute must have a valid & supported image extension.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',

View File

@@ -1,10 +1,12 @@
<?php
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'página creada',
'page_create_notification' => 'Página creada exitosamente',

View File

@@ -1,15 +1,21 @@
<?php
/**
* Authentication Language Lines
* The following language lines are used during authentication for various
* messages that we need to display to the user.
*/
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Las credenciales no concuerdan con nuestros registros.',
'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
// Login & Register
/**
* Login & Register
*/
'sign_up' => 'Registrarse',
'log_in' => 'Acceder',
'log_in_with' => 'Acceder con :socialDriver',
@@ -37,18 +43,23 @@ return [
'register_success' => '¡Gracias por registrarse! Ahora se encuentra registrado y logueado.',
// Password Reset
/**
* Password Reset
*/
'reset_password' => 'Resetear Contraseña',
'reset_password_send_instructions' => 'Introduzca su correo electrónico a continuación y le será enviado un correo con un link para la restauración',
'reset_password_send_button' => 'Enviar Enlace de Reseteo',
'reset_password_sent_success' => 'Un enlace para resetear la contraseña ha sido enviado a :email.',
'reset_password_success' => 'Su password ha sido reseteado de manera éxitosa.',
'email_reset_subject' => 'Resetee la contraseña de :appName',
'email_reset_text' => 'Está recibiendo este correo electrónico debido a que recibimos una solicitud de reseteo de contraseña de su cuenta.',
'email_reset_not_requested' => 'Si no ha solicitado un reseteo de la contraseña, no es requerida ninguna acción por su parte.',
// Email Confirmation
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Confirme su correo electrónico en :appName',
'email_confirm_greeting' => '¡Gracias por unirse a :appName!',
'email_confirm_text' => 'Por favor confirme su dirección de correo electrónico haciendo click en el siguiente botón:',
@@ -62,4 +73,4 @@ return [
'email_not_confirmed_click_link' => 'Por favor siga el enlace en el correo electrónico que ha sido enviado durante el proceso de registro.',
'email_not_confirmed_resend' => 'Si no puede encontrar el correo electrónico, puede solicitar el renvío del correo electrónico de confirmación rellenando el formulario que se muestra a continuación.',
'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
];
];

View File

@@ -1,10 +1,9 @@
<?php
/**
* Common elements found throughout many areas of BookStack.
*/
return [
// Buttons
/**
* Buttons
*/
'cancel' => 'Cancelar',
'confirm' => 'Confirmar',
'back' => 'Atrás',
@@ -13,14 +12,18 @@ return [
'select' => 'Seleccionar',
'more' => 'Más',
// Form Labels
/**
* Form Labels
*/
'name' => 'Nombre',
'description' => 'Descripción',
'role' => 'Rol',
'cover_image' => 'Imagen de portada',
'cover_image_description' => 'Esta imagen debe ser aproximadamente de 440x250px.',
// Actions
/**
* Actions
*/
'actions' => 'Acciones',
'view' => 'Ver',
'create' => 'Crear',
@@ -37,7 +40,9 @@ return [
'remove' => 'Remover',
'add' => 'Añadir',
// Misc
/**
* Misc
*/
'deleted_user' => 'Usuario borrado',
'no_activity' => 'Ninguna actividad para mostrar',
'no_items' => 'No hay elementos disponibles',
@@ -49,11 +54,15 @@ return [
'list_view' => 'Vista en Lista',
'default' => 'Predeterminada',
// Header
/**
* Header
*/
'view_profile' => 'Ver Perfil',
'edit_profile' => 'Editar Perfil',
// Email Content
/**
* Email Content
*/
'email_action_help' => 'Si está teniendo problemas clicando en el botón ":actionText", copie y pegue la siguiente URL en su navegador web:',
'email_rights' => 'Todos los derechos reservados',
];
];

View File

@@ -1,10 +1,9 @@
<?php
/**
* Text used in custom JavaScript driven components.
*/
return [
// Image Manager
/**
* Image Manager
*/
'image_select' => 'Seleccionar Imagen',
'image_all' => 'Todas',
'image_all_title' => 'Ver todas las imágenes',
@@ -25,7 +24,9 @@ return [
'image_delete_success' => 'Imagen borrada exitosamente',
'image_upload_remove' => 'Borrar',
// Code Editor
/**
* Code editor
*/
'code_editor' => 'Editar Código',
'code_language' => 'Lenguaje del Código',
'code_content' => 'Contenido del Código',

View File

@@ -1,11 +1,9 @@
<?php
/**
* Text used for 'Entities' (Document Structure Elements) such as
* Books, Shelves, Chapters & Pages
*/
return [
// Shared
/**
* Shared
*/
'recently_created' => 'Recientemente creado',
'recently_created_pages' => 'Páginas recientemente creadas',
'recently_updated_pages' => 'Páginas recientemente actualizadas',
@@ -33,13 +31,17 @@ return [
'export_pdf' => 'Archivo PDF',
'export_text' => 'Archivo de texto',
// Permissions and restrictions
/**
* Permissions and restrictions
*/
'permissions' => 'Permisos',
'permissions_intro' => 'Una vez habilitado, estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
'permissions_enable' => 'Habilitar permisos personalizados',
'permissions_save' => 'Guardar permisos',
// Search
/**
* Search
*/
'search_results' => 'Resultados de búsqueda',
'search_total_results_found' => 'Se han encontrado :count resultados|Se han encontrado :count resultados en total',
'search_clear' => 'Limpiar resultados',
@@ -64,7 +66,9 @@ return [
'search_set_date' => 'fecha',
'search_update' => 'Actualizar Búsqueda',
// Shelves
/**
* Shelves
*/
'shelf' => 'Estante',
'shelves' => 'Estantes',
'shelves_long' => 'Estantes',
@@ -94,7 +98,9 @@ return [
'shelves_copy_permissions_explain' => 'Esto aplicará los ajustes de permisos de este estante para todos sus libros. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante han sido guardados.',
'shelves_copy_permission_success' => 'Permisos del estante copiados a :count libros',
// Books
/**
* Books
*/
'book' => 'Libro',
'books' => 'Libros',
'x_books' => ':count Libro|:count Libros',
@@ -128,7 +134,9 @@ return [
'books_sort_show_other' => 'Mostrar otros libros',
'books_sort_save' => 'Guardar nuevo orden',
// Chapters
/**
* Chapters
*/
'chapter' => 'Capítulo',
'chapters' => 'Capítulos',
'x_chapters' => ':count Capítulo|:count Capítulos',
@@ -151,7 +159,9 @@ return [
'chapters_permissions_success' => 'Permisos de capítulo actualizados',
'chapters_search_this' => 'Buscar este capítulo',
// Pages
/**
* Pages
*/
'page' => 'Página',
'pages' => 'Páginas',
'x_pages' => ':count Página|:count Páginas',
@@ -225,7 +235,9 @@ return [
'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
'pages_specific' => 'Página específica',
// Editor Sidebar
/**
* Editor sidebar
*/
'page_tags' => 'Etiquetas de Página',
'chapter_tags' => 'Etiquetas de Capítulo',
'book_tags' => 'Etiquetas de Libro',
@@ -261,14 +273,18 @@ return [
'attachments_file_updated' => 'Fichero actualizado éxitosamente',
'attachments_link_attached' => 'Enlace agregado éxitosamente a la ágina',
// Profile View
/**
* Profile View
*/
'profile_user_for_x' => 'Usuario para :time',
'profile_created_content' => 'Contenido creado',
'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName no ha creado ningún libro',
// Comments
/**
* Comments
*/
'comment' => 'Comentario',
'comments' => 'Comentarios',
'comment_add' => 'Añadir Comentario',
@@ -286,8 +302,10 @@ return [
'comment_delete_confirm' => '¿Está seguro de que quiere borrar este comentario?',
'comment_in_reply_to' => 'En respuesta a :commentId',
// Revision
/**
* Revision
*/
'revision_delete_confirm' => '¿Está seguro de que desea eliminar esta revisión?',
'revision_delete_success' => 'Revisión eliminada',
'revision_cannot_delete_latest' => 'No se puede eliminar la última revisión.'
];
];

View File

@@ -1,9 +1,11 @@
<?php
/**
* Text shown in error messaging.
*/
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'No tiene permisos para visualizar la página solicitada.',
'permissionJson' => 'No tiene permisos para ejecutar la acción solicitada.',
@@ -79,5 +81,4 @@ return [
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'Volverá a estar operativa pronto.',
];

View File

@@ -1,11 +1,18 @@
<?php
/**
* Pagination Language Lines
* The following language lines are used by the paginator library to build
* the simple pagination links.
*/
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Siguiente &raquo;',

View File

@@ -1,11 +1,18 @@
<?php
/**
* Password Reminder Language Lines
* The following language lines are the default lines which match reasons
* that are given by the password broker for a password update attempt has failed.
*/
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'La contraseña debe ser como mínimo de seis caracteres y coincidir con la confirmación.',
'user' => "No podemos encontrar un usuario con esta dirección de correo electrónico.",
'token' => 'El token de reseteo de la contraseña es inválido.',

View File

@@ -1,17 +1,21 @@
<?php
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
return [
// Common Messages
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Ajustes',
'settings_save' => 'Guardar ajustes',
'settings_save_success' => 'Ajustes guardados',
// App Settings
/**
* App settings
*/
'app_settings' => 'Ajustes de la aplicación',
'app_name' => 'Nombre de la aplicación',
'app_name_desc' => 'Este nombre se muestra en la cabecera y en cualquier correo electrónico',
@@ -33,7 +37,10 @@ return [
'app_disable_comments' => 'Deshabilitar comentarios',
'app_disable_comments_desc' => 'Deshabilita los comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',
// Registration Settings
/**
* Registration settings
*/
'reg_settings' => 'Ajustes de registro',
'reg_allow' => '¿Permitir registro?',
'reg_default_role' => 'Rol de usuario por defecto después del registro',
@@ -43,7 +50,10 @@ return [
'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los dominio a los que les gustaría restringir el registro de usuarios. A los usuarios les será enviado un correo electrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Tenga en cuenta que los usuarios podrán cambiar sus direcciones de correo electrónico después de registrarse exitosamente.',
'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
// Maintenance settings
/**
* Maintenance settings
*/
'maint' => 'Mantenimiento',
'maint_image_cleanup' => 'Limpiar imágenes',
'maint_image_cleanup_desc' => "Analiza las páginas y sus revisiones para comprobar qué imágenes y dibujos están siendo utilizadas y cuales no son necesarias. Asegúrate de crear una copia completa de la base de datos y de las imágenes antes de lanzar esta opción.",
@@ -53,7 +63,10 @@ return [
'maint_image_cleanup_success' => '¡Se han encontrado y borrado :count imágenes posiblemente no utilizadas!',
'maint_image_cleanup_nothing_found' => '¡No se han encontrado imágenes sin utilizar, no se han borrado imágenes!',
// Role Settings
/**
* Role settings
*/
'roles' => 'Roles',
'role_user_roles' => 'Roles de usuario',
'role_create' => 'Crear nuevo rol',
@@ -86,7 +99,10 @@ return [
'role_users' => 'Usuarios en este rol',
'role_users_none' => 'No hay usuarios asignados a este rol',
// Users
/**
* Users
*/
'users' => 'Usuarios',
'user_profile' => 'Perfil de usuario',
'users_add_new' => 'Agregar nuevo usuario',

View File

@@ -1,13 +1,18 @@
<?php
/**
* Validation Lines
* The following language lines contain the default error messages used by
* the validator class. Some of these rules have multiple versions such
* as the size rules. Feel free to tweak each of these messages here.
*/
return [
// Standard laravel validation lines
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'El :attribute debe ser aceptado.',
'active_url' => 'El :attribute no es una URL válida.',
'after' => 'El :attribute debe ser una fecha posterior :date.',
@@ -70,13 +75,34 @@ return [
'unique' => 'El atributo :attribute ya ha sido tomado.',
'url' => 'El atributo :attribute tiene un formato inválido.',
// Custom validation lines
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'Requerida confirmación de contraseña',
],
],
// Custom validation attributes
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@@ -27,7 +27,7 @@ return [
'email' => 'Email',
'password' => 'Wachtwoord',
'password_confirm' => 'Wachtwoord Bevestigen',
'password_hint' => 'Minimaal 6 tekens',
'password_hint' => 'Minimaal 5 tekens',
'forgot_password' => 'Wachtwoord vergeten?',
'remember_me' => 'Mij onthouden',
'ldap_email_hint' => 'Geef een email op waarmee je dit account wilt gebruiken.',
@@ -73,4 +73,4 @@ return [
'email_not_confirmed_click_link' => 'Klik op de link in de e-mail die vlak na je registratie is verstuurd.',
'email_not_confirmed_resend' => 'Als je deze e-mail niet kunt vinden kun je deze met onderstaande formulier opnieuw verzenden.',
'email_not_confirmed_resend_button' => 'Bevestigingsmail Opnieuw Verzenden',
];
];

View File

@@ -48,20 +48,14 @@
</form>
</div>
<div class="links text-center">
@if(userCanOnAny('view', \BookStack\Entities\Bookshelf::class) || userCan('bookshelf-view-own'))
@if(userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ baseUrl('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif
<a href="{{ baseUrl('/books') }}">@icon('book'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage'))
<a href="{{ baseUrl('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
@endif
@if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
<a href="{{ baseUrl('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
@endif
@if(!signedInUser())
@if(setting('registration-enabled', false))
<a href="{{ baseUrl("/register") }}">@icon('new-user') {{ trans('auth.sign_up') }}</a>
@endif
<a href="{{ baseUrl('/login') }}">@icon('login') {{ trans('auth.log_in') }}</a>
@endif
</div>

View File

@@ -20,11 +20,11 @@
@if(userCan('chapter-update', $chapter))
<a href="{{ $chapter->getUrl('/edit') }}" class="text-primary text-button">@icon('edit'){{ trans('common.edit') }}</a>
@endif
@if((userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter) )|| userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
@if(userCan('chapter-update', $chapter) || userCan('restrictions-manage', $chapter) || userCan('chapter-delete', $chapter))
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
<ul>
@if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
@if(userCan('chapter-update', $chapter))
<li><a href="{{ $chapter->getUrl('/move') }}" class="text-primary">@icon('folder'){{ trans('common.move') }}</a></li>
@endif
@if(userCan('restrictions-manage', $chapter))

View File

@@ -21,9 +21,7 @@
<a @click="updateLanguage('Java')">Java</a>
<a @click="updateLanguage('JavaScript')">JavaScript</a>
<a @click="updateLanguage('JSON')">JSON</a>
<a @click="updateLanguage('Lua')">Lua</a>
<a @click="updateLanguage('PHP')">PHP</a>
<a @click="updateLanguage('Powershell')">Powershell</a>
<a @click="updateLanguage('MarkDown')">MarkDown</a>
<a @click="updateLanguage('Nginx')">Nginx</a>
<a @click="updateLanguage('Python')">Python</a>
@@ -50,4 +48,4 @@
</div>
</div>
</div>
</div>

View File

@@ -34,7 +34,7 @@
@endif
</td>
<td> @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif</td>
<td><small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td><small>{{ $revision->created_at->format('jS F, Y H:i:s') }} <br> ({{ $revision->created_at->diffForHumans() }})</small></td>
<td>{{ $revision->summary }}</td>
<td class="actions">
<a href="{{ $revision->getUrl('changes') }}" target="_blank">{{ trans('entities.pages_revisions_changes') }}</a>

View File

@@ -17,17 +17,13 @@
@if(userCan('page-update', $page))
<a href="{{ $page->getUrl('/edit') }}" class="text-primary text-button" >@icon('edit'){{ trans('common.edit') }}</a>
@endif
@if((userCan('page-view', $page) && userCanOnAny('page-create')) || userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
@if(userCan('page-update', $page) || userCan('restrictions-manage', $page) || userCan('page-delete', $page))
<div dropdown class="dropdown-container">
<a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
<ul>
@if(userCanOnAny('page-create'))
<li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
@endif
@if(userCan('page-delete', $page) && userCan('page-update', $page))
<li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
@endif
@if(userCan('page-update', $page))
<li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
<li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
<li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
@endif
@if(userCan('restrictions-manage', $page))

View File

@@ -38,9 +38,8 @@
<div class="form-group">
<label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
<select name="setting[language]" id="user-language">
@foreach(trans('settings.language_select') as $lang => $label)
<option @if(setting()->getUser($user, 'language', config('app.default_locale')) === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
<option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
@endforeach
</select>
</div>

View File

@@ -37,27 +37,22 @@
</div>
<div class="col-md-5 text-bigger" id="content-counts">
<div class="text-muted">{{ trans('entities.profile_created_content') }}</div>
<a href="#recent-books">
<div class="text-book">
@icon('book') {{ trans_choice('entities.x_books', $assetCounts['books']) }}
</div>
</a>
<a href="#recent-chapters">
<div class="text-chapter">
@icon('chapter') {{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}
</div>
</a>
<a href="#recent-pages">
<div class="text-page">
@icon('page') {{ trans_choice('entities.x_pages', $assetCounts['pages']) }}
</div>
</a>
<div class="text-book">
@icon('book') {{ trans_choice('entities.x_books', $assetCounts['books']) }}
</div>
<div class="text-chapter">
@icon('chapter') {{ trans_choice('entities.x_chapters', $assetCounts['chapters']) }}
</div>
<div class="text-page">
@icon('page') {{ trans_choice('entities.x_pages', $assetCounts['pages']) }}
</div>
</div>
</div>
<hr class="even">
<h3 id="recent-pages">{{ trans('entities.recently_created_pages') }}</h3>
<h3>{{ trans('entities.recently_created_pages') }}</h3>
@if (count($recentlyCreated['pages']) > 0)
@include('partials/entity-list', ['entities' => $recentlyCreated['pages']])
@else
@@ -65,7 +60,8 @@
@endif
<hr class="even">
<h3 id="recent-chapters">{{ trans('entities.recently_created_chapters') }}</h3>
<h3>{{ trans('entities.recently_created_chapters') }}</h3>
@if (count($recentlyCreated['chapters']) > 0)
@include('partials/entity-list', ['entities' => $recentlyCreated['chapters']])
@else
@@ -73,7 +69,8 @@
@endif
<hr class="even">
<h3 id="recent-books">{{ trans('entities.recently_created_books') }}</h3>
<h3>{{ trans('entities.recently_created_books') }}</h3>
@if (count($recentlyCreated['books']) > 0)
@include('partials/entity-list', ['entities' => $recentlyCreated['books']])
@else

View File

@@ -23,7 +23,6 @@ class LdapTest extends BrowserKitTest
'auth.method' => 'ldap',
'services.ldap.base_dn' => 'dc=ldap,dc=local',
'services.ldap.email_attribute' => 'mail',
'services.ldap.display_name_attribute' => 'cn',
'services.ldap.user_to_groups' => false,
'auth.providers.users.driver' => 'ldap',
]);
@@ -46,15 +45,6 @@ class LdapTest extends BrowserKitTest
});
}
protected function mockUserLogin()
{
return $this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In');
}
public function test_login()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
@@ -70,7 +60,11 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->mockUserLogin()
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
@@ -96,7 +90,11 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
$this->mockEscapes(2);
$this->mockUserLogin()
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
@@ -117,7 +115,11 @@ class LdapTest extends BrowserKitTest
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
$this->mockEscapes(2);
$this->mockUserLogin()
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/login')->see('These credentials do not match our records.')
->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
}
@@ -194,7 +196,12 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(5);
$this->mockExplodes(6);
$this->mockUserLogin()->seePageIs('/');
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -242,7 +249,12 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(4);
$this->mockExplodes(2);
$this->mockUserLogin()->seePageIs('/');
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -291,7 +303,12 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(4);
$this->mockExplodes(2);
$this->mockUserLogin()->seePageIs('/');
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -337,7 +354,12 @@ class LdapTest extends BrowserKitTest
$this->mockEscapes(5);
$this->mockExplodes(6);
$this->mockUserLogin()->seePageIs('/');
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
@@ -350,63 +372,4 @@ class LdapTest extends BrowserKitTest
]);
}
public function test_login_uses_specified_display_name_attribute()
{
app('config')->set([
'services.ldap.display_name_attribute' => 'displayName'
]);
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')],
'displayName' => 'displayNameAttribute'
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->mockUserLogin()
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
->press('Log In')
->seePageIs('/')
->see('displayNameAttribute')
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
}
public function test_login_uses_default_display_name_attribute_if_specified_not_present()
{
app('config')->set([
'services.ldap.display_name_attribute' => 'displayName'
]);
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->once();
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->mockEscapes(4);
$this->mockUserLogin()
->seePageIs('/login')->see('Please enter an email to use for this account.');
$this->type($this->mockUser->email, '#email')
->press('Log In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
}
}

View File

@@ -1,7 +1,5 @@
<?php namespace Tests;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
@@ -29,22 +27,6 @@ class BookShelfTest extends TestCase
$resp->assertElementContains('header', 'Shelves');
}
public function test_shelves_shows_in_header_if_have_any_shelve_view_permission()
{
$user = factory(User::class)->create();
$this->giveUserPermissions($user, ['image-create-all']);
$shelf = Bookshelf::first();
$userRole = $user->roles()->first();
$resp = $this->actingAs($user)->get('/');
$resp->assertElementNotContains('header', 'Shelves');
$this->setEntityRestrictions($shelf, ['view'], [$userRole]);
$resp = $this->get('/');
$resp->assertElementContains('header', 'Shelves');
}
public function test_shelves_page_contains_create_link()
{
$resp = $this->asEditor()->get('/shelves');

View File

@@ -134,4 +134,52 @@ class ExportTest extends TestCase
$resp->assertDontSee($page->updated_at->diffForHumans());
}
public function test_html_export_media_protocol_updated()
{
$page = Page::first();
$page->html = '<p id="bkmrk-%C2%A0-0">&nbsp;</p><p id="bkmrk-%C2%A0-1"><iframe src="//www.youtube.com/embed/LkFt_fp7FmE" width="560" height="314" allowfullscreen="allowfullscreen"></iframe></p><p id="bkmrk-"><iframe src="//player.vimeo.com/video/276396369?title=0&amp;amp;byline=0" width="425" height="350" allowfullscreen="allowfullscreen"></iframe></p><p id="bkmrk--0"><iframe style="border: 0;" src="//maps.google.com/embed?testquery=true" width="600" height="450" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p><p id="bkmrk--1"><iframe src="//www.dailymotion.com/embed/video/x2rqgfm" width="480" height="432" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p><p id="bkmrk-%C2%A0-2">&nbsp;</p>';
$page->save();
$this->asEditor();
$resp = $this->get($page->getUrl('/export/html'));
$resp->assertStatus(200);
$checks = [
'https://www.youtube.com/embed/LkFt_fp7FmE',
'https://player.vimeo.com/video/276396369?title=0&amp;amp;byline=0',
'https://maps.google.com/embed?testquery=true',
'https://www.dailymotion.com/embed/video/x2rqgfm',
];
foreach ($checks as $check) {
$resp->assertSee($check);
}
}
public function test_pdf_export_no_video_iframe() {
$page = Page::first();
$page->html = '<p id="bkmrk-%C2%A0-0">&nbsp;</p>' .
'<p id="bkmrk-%C2%A0-1"><iframe src="//www.youtube.com/embed/LkFt_fp7FmE" width="560" height="314" allowfullscreen="allowfullscreen"></iframe></p>' .
'<p id="bkmrk-"><video src="//player.vimeo.com/video/276396369?title=0&amp;amp;byline=0" width="425" height="350" allowfullscreen="allowfullscreen"></video></p>' .
'<p id="bkmrk--0"><iframe style="border: 0;" src="//maps.google.com/embed?testquery=true" width="600" height="450" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p>' .
'<p id="bkmrk--1"><iframe src="//www.dailymotion.com/embed/video/x2rqgfm" width="480" height="432" frameborder="0" allowfullscreen="allowfullscreen"></iframe></p>' .
'<p id="bkmrk-%C2%A0-2">&nbsp;</p>';
$page->save();
$this->asEditor();
$resp = $this->get($page->getUrl('/export/pdf?isTesting=true'));
$resp->assertStatus(200);
$checks = [
'</video>',
'</iframe>'
];
foreach ($checks as $check) {
$resp->assertDontSee($check);
}
}
}

View File

@@ -40,18 +40,15 @@ class PageContentTest extends TestCase
{
$page = Page::first();
$secondPage = Page::where('id', '!=', $page->id)->first();
$this->asEditor();
$includeTag = '{{@' . $secondPage->id . '}}';
$page->html = '<p>' . $includeTag . '</p>';
$page->html = "<p>{{@$secondPage->id}}</p>";
$resp = $this->put($page->getUrl(), ['name' => $page->name, 'html' => $page->html, 'summary' => '']);
$resp->assertStatus(302);
$page = Page::find($page->id);
$this->assertContains($includeTag, $page->html);
$this->assertEquals('', $page->text);
$this->assertContains("{{@$secondPage->id}}", $page->html);
}
public function test_page_includes_do_not_break_tables()

View File

@@ -1,9 +1,9 @@
<?php namespace Tests;
use BookStack\Auth\Role;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\Repos\PageRepo;
class SortTest extends TestCase
@@ -58,14 +58,14 @@ class SortTest extends TestCase
$newBook = Book::where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'delete'], $editor->roles);
$this->setEntityRestrictions($newBook, ['view', 'edit', 'delete'], $editor->roles);
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$this->assertPermissionError($movePageResp);
$this->setEntityRestrictions($newBook, ['view', 'update', 'delete', 'create'], $editor->roles);
$this->setEntityRestrictions($newBook, ['view', 'edit', 'delete', 'create'], $editor->roles);
$movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
@@ -76,33 +76,6 @@ class SortTest extends TestCase
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
}
public function test_page_move_requires_delete_permissions()
{
$page = Page::first();
$currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles);
$this->setEntityRestrictions($page, ['view', 'update', 'create'], $editor->roles);
$movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$this->assertPermissionError($movePageResp);
$pageView = $this->get($page->getUrl());
$pageView->assertDontSee($page->getUrl('/move'));
$this->setEntityRestrictions($page, ['view', 'update', 'create', 'delete'], $editor->roles);
$movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$page = Page::find($page->id);
$movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
}
public function test_chapter_move()
{
$chapter = Chapter::first();
@@ -131,33 +104,6 @@ class SortTest extends TestCase
$pageCheckResp->assertSee($newBook->name);
}
public function test_chapter_move_requires_delete_permissions()
{
$chapter = Chapter::first();
$currentBook = $chapter->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first();
$editor = $this->getEditor();
$this->setEntityRestrictions($newBook, ['view', 'update', 'create', 'delete'], $editor->roles);
$this->setEntityRestrictions($chapter, ['view', 'update', 'create'], $editor->roles);
$moveChapterResp = $this->actingAs($editor)->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$this->assertPermissionError($moveChapterResp);
$pageView = $this->get($chapter->getUrl());
$pageView->assertDontSee($chapter->getUrl('/move'));
$this->setEntityRestrictions($chapter, ['view', 'update', 'create', 'delete'], $editor->roles);
$moveChapterResp = $this->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
$chapter = Chapter::find($chapter->id);
$moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id == $newBook->id, 'Page book is now the new book');
}
public function test_book_sort()
{
$oldBook = Book::query()->first();
@@ -240,35 +186,4 @@ class SortTest extends TestCase
$this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
}
public function test_page_can_be_copied_without_edit_permission()
{
$page = Page::first();
$currentBook = $page->book;
$newBook = Book::where('id', '!=', $currentBook->id)->first();
$viewer = $this->getViewer();
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertDontSee($page->getUrl('/copy'));
$newBook->created_by = $viewer->id;
$newBook->save();
$this->giveUserPermissions($viewer, ['page-create-own']);
$this->regenEntityPermissions($newBook);
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertSee($page->getUrl('/copy'));
$movePageResp = $this->post($page->getUrl('/copy'), [
'entity_selection' => 'book:' . $newBook->id,
'name' => 'My copied test page'
]);
$movePageResp->assertRedirect();
$this->assertDatabaseHas('pages', [
'name' => 'My copied test page',
'created_by' => $viewer->id,
'book_id' => $newBook->id,
]);
}
}

View File

@@ -109,16 +109,6 @@ class RolesTest extends BrowserKitTest
->seePageIs('/settings/users');
}
public function test_manage_users_permission_shows_link_in_header_if_does_not_have_settings_manage_permision()
{
$usersLink = 'href="'.url('/settings/users') . '"';
$this->actingAs($this->user)->visit('/')->dontSee($usersLink);
$this->giveUserPermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->visit('/')->see($usersLink);
$this->giveUserPermissions($this->user, ['settings-manage', 'users-manage']);
$this->actingAs($this->user)->visit('/')->dontSee($usersLink);
}
public function test_user_roles_manage_permission()
{
$this->actingAs($this->user)->visit('/settings/roles')

View File

@@ -14,24 +14,6 @@ class PublicActionTest extends BrowserKitTest
$this->visit($page->getUrl())->seePageIs('/login');
}
public function test_login_link_visible()
{
$this->setSettings(['app-public' => 'true']);
$this->visit('/')->see(url('/login'));
}
public function test_register_link_visible_when_enabled()
{
$this->setSettings(['app-public' => 'true']);
$this->visit('/')->see(url('/login'));
$this->visit('/')->dontSee(url('/register'));
$this->setSettings(['app-public' => 'true', 'registration-enabled' => 'true']);
$this->visit('/')->see(url('/login'));
$this->visit('/')->see(url('/register'));
}
public function test_books_viewable()
{
$this->setSettings(['app-public' => 'true']);

View File

@@ -39,57 +39,13 @@ class ImageTest extends TestCase
]);
}
public function test_php_files_cannot_be_uploaded()
{
$page = Page::first();
$admin = $this->getAdmin();
$this->actingAs($admin);
$fileName = 'bad.php';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$file = $this->getTestImage($fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
$this->assertDatabaseMissing('images', [
'type' => 'gallery',
'name' => $fileName
]);
}
public function test_php_like_files_cannot_be_uploaded()
{
$page = Page::first();
$admin = $this->getAdmin();
$this->actingAs($admin);
$fileName = 'bad.phtml';
$relPath = $this->getTestImagePath('gallery', $fileName);
$this->deleteImage($relPath);
$file = $this->getTestImage($fileName);
$upload = $this->withHeader('Content-Type', 'image/jpeg')->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $file], []);
$upload->assertStatus(302);
$this->assertFalse(file_exists(public_path($relPath)), 'Uploaded php file was uploaded but should have been stopped');
$this->assertDatabaseMissing('images', [
'type' => 'gallery',
'name' => $fileName
]);
}
public function test_secure_images_uploads_to_correct_place()
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$galleryFile = $this->getTestImage('my-secure-test-upload');
$page = Page::first();
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);
@@ -105,9 +61,9 @@ class ImageTest extends TestCase
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-secure-test-upload.png');
$galleryFile = $this->getTestImage('my-secure-test-upload');
$page = Page::first();
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload.png');
$expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
$upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$imageUrl = json_decode($upload->getContent(), true)['url'];
@@ -128,9 +84,9 @@ class ImageTest extends TestCase
{
config()->set('filesystems.default', 'local_secure');
$this->asEditor();
$galleryFile = $this->getTestImage('my-system-test-upload.png');
$galleryFile = $this->getTestImage('my-system-test-upload');
$page = Page::first();
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload.png');
$expectedPath = public_path('uploads/images/system/' . Date('Y-m-M') . '/my-system-test-upload');
$upload = $this->call('POST', '/images/system/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
$upload->assertStatus(200);

View File

@@ -1,8 +1,6 @@
<?php namespace Tests\Uploads;
use Illuminate\Http\UploadedFile;
trait UsesImages
{
/**
@@ -17,11 +15,11 @@ trait UsesImages
/**
* Get a test image that can be uploaded
* @param $fileName
* @return UploadedFile
* @return \Illuminate\Http\UploadedFile
*/
protected function getTestImage($fileName)
{
return new UploadedFile($this->getTestImageFilePath(), $fileName, 'image/png', 5238, null, true);
return new \Illuminate\Http\UploadedFile($this->getTestImageFilePath(), $fileName, 'image/png', 5238);
}
/**
@@ -48,14 +46,12 @@ trait UsesImages
* Uploads an image with the given name.
* @param $name
* @param int $uploadedTo
* @param string $contentType
* @return \Illuminate\Foundation\Testing\TestResponse
*/
protected function uploadImage($name, $uploadedTo = 0, $contentType = 'image/png')
protected function uploadImage($name, $uploadedTo = 0)
{
$file = $this->getTestImage($name);
return $this->withHeader('Content-Type', $contentType)
->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
return $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
}
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 B

Some files were not shown because too many files have changed in this diff Show More