mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-05 08:39:55 +03:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d8fa1e5b | ||
|
|
18562f1e10 | ||
|
|
fdabafffda | ||
|
|
e2df15fe20 | ||
|
|
54bac17ef0 | ||
|
|
7634ac4e12 | ||
|
|
c4f5ab12cf | ||
|
|
57a063cdfb | ||
|
|
1fa90e4f12 | ||
|
|
d62cdd58d3 | ||
|
|
ed6ec341df | ||
|
|
0cfff6ab6f | ||
|
|
7ca66c5d5e | ||
|
|
9cbea1eb08 | ||
|
|
1a2d374f24 | ||
|
|
e32929029b | ||
|
|
eb76e882c5 | ||
|
|
d326417edc | ||
|
|
a3a8fef6b2 | ||
|
|
0c16334426 | ||
|
|
600f8cd142 | ||
|
|
5c8c85a0ff | ||
|
|
7a6f21648a | ||
|
|
df0e03cd07 | ||
|
|
85db812fea | ||
|
|
fb5b5e138d | ||
|
|
3eaf03a7ac | ||
|
|
5420f3451c | ||
|
|
7d94da10fb | ||
|
|
86090a694f | ||
|
|
1ee8287c73 | ||
|
|
c7322a71f7 | ||
|
|
2c3523f6a1 | ||
|
|
dd6076049c | ||
|
|
ba8ba5c634 | ||
|
|
c2069f37cc | ||
|
|
1e0aa7ee2c | ||
|
|
27942f5ce8 | ||
|
|
d0ff79ea60 | ||
|
|
3de02566bf | ||
|
|
93fd869ba3 | ||
|
|
3ca149137e | ||
|
|
db9aa41096 | ||
|
|
bf8e7f3393 | ||
|
|
8eb98cd591 | ||
|
|
0f9ba21b05 | ||
|
|
7a059a5e90 | ||
|
|
e5fc104aff | ||
|
|
d0ed165630 | ||
|
|
68ef6a842f | ||
|
|
c1f070a136 | ||
|
|
c2cc1ec5e5 | ||
|
|
386925ad8e | ||
|
|
243c1db408 | ||
|
|
834f8e7046 | ||
|
|
32e3399334 | ||
|
|
9e7bcacf8c | ||
|
|
7be7d7d1e7 | ||
|
|
04c1d0e071 | ||
|
|
ab62e0f75b | ||
|
|
d85f99c87c | ||
|
|
c42b6aece9 | ||
|
|
7f8f3080c5 | ||
|
|
9cf4191079 | ||
|
|
b8e2d75014 | ||
|
|
f522f16526 | ||
|
|
b010d2663d | ||
|
|
a083ceaf44 | ||
|
|
95798a2eba | ||
|
|
43b6633183 | ||
|
|
c50ac022a8 | ||
|
|
a3d36237e2 | ||
|
|
a2be61f26d | ||
|
|
79f5b579d7 | ||
|
|
66ecee1e26 | ||
|
|
02e86ea18f | ||
|
|
723dbe1da7 | ||
|
|
65fe89441f | ||
|
|
2093122ac5 | ||
|
|
ab584c93bc | ||
|
|
2d8698a218 | ||
|
|
454fb883a2 | ||
|
|
fc504a3d2c | ||
|
|
dd805503fb | ||
|
|
f24336f77a | ||
|
|
aa6a752e38 | ||
|
|
83b576eb19 | ||
|
|
c4e31a0d5e | ||
|
|
f8cdd6e80d | ||
|
|
99c42033b1 | ||
|
|
aad2ee675c | ||
|
|
a192b600fc | ||
|
|
b714652e10 | ||
|
|
034478409e |
@@ -200,6 +200,7 @@ LDAP_TLS_INSECURE=false
|
||||
LDAP_ID_ATTRIBUTE=uid
|
||||
LDAP_EMAIL_ATTRIBUTE=mail
|
||||
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
|
||||
LDAP_THUMBNAIL_ATTRIBUTE=null
|
||||
LDAP_FOLLOW_REFERRALS=true
|
||||
LDAP_DUMP_USER_DETAILS=false
|
||||
|
||||
@@ -222,6 +223,7 @@ SAML2_IDP_x509=null
|
||||
SAML2_ONELOGIN_OVERRIDES=null
|
||||
SAML2_DUMP_USER_DETAILS=false
|
||||
SAML2_AUTOLOAD_METADATA=false
|
||||
SAML2_IDP_AUTHNCONTEXT=true
|
||||
|
||||
# SAML group sync configuration
|
||||
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
|
||||
|
||||
8
.github/translators.txt
vendored
8
.github/translators.txt
vendored
@@ -54,6 +54,7 @@ Name :: Languages
|
||||
@benediktvolke :: German
|
||||
@Baptistou :: French
|
||||
@arcoai :: Spanish
|
||||
@Jokuna :: Korean
|
||||
cipi1965 :: Italian
|
||||
Mykola Ronik (Mantikor) :: Ukrainian
|
||||
furkanoyk :: Turkish
|
||||
@@ -157,3 +158,10 @@ HenrijsS :: Latvian
|
||||
Pascal R-B (pborgner) :: German
|
||||
Boris (Ginfred) :: Russian
|
||||
Jonas Anker Rasmussen (jonasanker) :: Danish
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
|
||||
kometchtech :: Japanese
|
||||
Auri (Atalonica) :: Catalan
|
||||
Francesco Franchina (ffranchina) :: Italian
|
||||
Aimrane Kds (aimrane.kds) :: Arabic
|
||||
whenwesober :: Indonesian
|
||||
Rem (remkovdhoef) :: Dutch
|
||||
|
||||
17
app/Actions/Favourite.php
Normal file
17
app/Actions/Favourite.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Favourite extends Model
|
||||
{
|
||||
protected $fillable = ['user_id'];
|
||||
|
||||
/**
|
||||
* Get the related model that can be favourited.
|
||||
*/
|
||||
public function favouritable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
@@ -9,10 +10,25 @@ class Tag extends Model
|
||||
|
||||
/**
|
||||
* Get the entity that this tag belongs to
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphTo
|
||||
{
|
||||
return $this->morphTo('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name search for this tag name.
|
||||
*/
|
||||
public function nameUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a full URL to start a tag name and value search for this tag's values.
|
||||
*/
|
||||
public function valueUrl(): string
|
||||
{
|
||||
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
/**
|
||||
* Class View
|
||||
* Views are stored per-item per-person within the database.
|
||||
* They can be used to find popular items or recently viewed items
|
||||
* at a per-person level. They do not record every view instance as an
|
||||
* activity. Only the latest and original view times could be recognised.
|
||||
*
|
||||
* @property int $views
|
||||
* @property int $user_id
|
||||
*/
|
||||
class View extends Model
|
||||
{
|
||||
|
||||
@@ -9,10 +21,37 @@ class View extends Model
|
||||
|
||||
/**
|
||||
* Get all owning viewable models.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
|
||||
*/
|
||||
public function viewable()
|
||||
public function viewable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the current user's view count for the given viewable model.
|
||||
*/
|
||||
public static function incrementFor(Viewable $viewable): int
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** @var View $view */
|
||||
$view = $viewable->views()->firstOrNew([
|
||||
'user_id' => $user->id,
|
||||
], ['views' => 0]);
|
||||
|
||||
$view->forceFill(['views' => $view->views + 1])->save();
|
||||
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all views from the system.
|
||||
*/
|
||||
public static function clearAll()
|
||||
{
|
||||
static::query()->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use DB;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
protected $view;
|
||||
protected $permissionService;
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
|
||||
{
|
||||
$this->view = $view;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a view to the given entity.
|
||||
* @param \BookStack\Entities\Models\Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
public function add(Entity $entity)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return 0;
|
||||
}
|
||||
$view = $entity->views()->where('user_id', '=', $user->id)->first();
|
||||
// Add view if model exists
|
||||
if ($view) {
|
||||
$view->increment('views');
|
||||
return $view->views;
|
||||
}
|
||||
|
||||
// Otherwise create new view count
|
||||
$entity->views()->save($this->view->newInstance([
|
||||
'user_id' => $user->id,
|
||||
'views' => 1
|
||||
]));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param string|array $filterModels
|
||||
* @param string $action - used for permission checking
|
||||
* @return Collection
|
||||
*/
|
||||
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService
|
||||
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip($skipCount)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recently viewed entities for the current user.
|
||||
*/
|
||||
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$all = collect();
|
||||
/** @var Entity $instance */
|
||||
foreach ($this->entityProvider->all() as $name => $instance) {
|
||||
$items = $instance::visible()->withLastView()
|
||||
->orderBy('last_viewed_at', 'desc')
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get();
|
||||
$all = $all->concat($items);
|
||||
}
|
||||
|
||||
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all view counts by deleting all views.
|
||||
*/
|
||||
public function resetAll()
|
||||
{
|
||||
$this->view->truncate();
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
$this->ldapService->syncGroups($user, $username);
|
||||
}
|
||||
|
||||
// Attach avatar if non-existent
|
||||
if (is_null($user->avatar)) {
|
||||
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
|
||||
}
|
||||
|
||||
$this->login($user, $remember);
|
||||
return true;
|
||||
}
|
||||
@@ -115,6 +120,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||
'password' => Str::random(32),
|
||||
];
|
||||
|
||||
return $this->registrationService->registerUser($details, null, false);
|
||||
$user = $this->registrationService->registerUser($details, null, false);
|
||||
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\JsonDebugException;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use ErrorException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
@@ -14,15 +16,17 @@ class LdapService extends ExternalAuthService
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $userAvatars;
|
||||
protected $config;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->userAvatars = $userAvatars;
|
||||
$this->config = config('services.ldap');
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
@@ -76,10 +80,13 @@ class LdapService extends ExternalAuthService
|
||||
$idAttr = $this->config['id_attribute'];
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$displayNameAttr = $this->config['display_name_attribute'];
|
||||
$thumbnailAttr = $this->config['thumbnail_attribute'];
|
||||
|
||||
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
|
||||
$user = $this->getUserWithAttributes($userName, array_filter([
|
||||
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
|
||||
]));
|
||||
|
||||
if ($user === null) {
|
||||
if (is_null($user)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,6 +96,7 @@ class LdapService extends ExternalAuthService
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
|
||||
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
|
||||
];
|
||||
|
||||
if ($this->config['dump_user_details']) {
|
||||
@@ -350,4 +358,22 @@ class LdapService extends ExternalAuthService
|
||||
$userLdapGroups = $this->getUserGroups($username);
|
||||
$this->syncWithGroups($user, $userLdapGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save and attach an avatar image, if found in the ldap details, and attach
|
||||
* to the given user model.
|
||||
*/
|
||||
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
|
||||
{
|
||||
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = $ldapUserDetails['avatar'];
|
||||
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
|
||||
} catch (\Exception $exception) {
|
||||
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,37 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
/**
|
||||
* The core socialite library used.
|
||||
* @var Socialite
|
||||
*/
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
|
||||
/**
|
||||
* The default built-in social drivers we support.
|
||||
* @var string[]
|
||||
*/
|
||||
protected $validSocialDrivers = [
|
||||
'google',
|
||||
'github',
|
||||
'facebook',
|
||||
'slack',
|
||||
'twitter',
|
||||
'azure',
|
||||
'okta',
|
||||
'gitlab',
|
||||
'twitch',
|
||||
'discord'
|
||||
];
|
||||
|
||||
/**
|
||||
* Callbacks to run when configuring a social driver
|
||||
* for an initial redirect action.
|
||||
* Array is keyed by social driver name.
|
||||
* Callbacks are passed an instance of the driver.
|
||||
* @var array<string, callable>
|
||||
*/
|
||||
protected $configureForRedirectCallbacks = [];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
@@ -39,7 +66,7 @@ class SocialAuthService
|
||||
public function startLogIn(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +76,7 @@ class SocialAuthService
|
||||
public function startRegister(string $socialDriver): RedirectResponse
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
return $this->getDriverForRedirect($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +254,7 @@ class SocialAuthService
|
||||
/**
|
||||
* Provide redirect options per service for the Laravel Socialite driver
|
||||
*/
|
||||
public function getSocialDriver(string $driverName): Provider
|
||||
protected function getDriverForRedirect(string $driverName): Provider
|
||||
{
|
||||
$driver = $this->socialite->driver($driverName);
|
||||
|
||||
@@ -238,6 +265,10 @@ class SocialAuthService
|
||||
$driver->with(['resource' => 'https://graph.windows.net']);
|
||||
}
|
||||
|
||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
|
||||
@@ -248,12 +279,19 @@ class SocialAuthService
|
||||
* within the `Config/services.php` file.
|
||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||
*/
|
||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
|
||||
{
|
||||
public function addSocialDriver(
|
||||
string $driverName,
|
||||
array $config,
|
||||
string $socialiteHandler,
|
||||
callable $configureForRedirect = null
|
||||
) {
|
||||
$this->validSocialDrivers[] = $driverName;
|
||||
config()->set('services.' . $driverName, $config);
|
||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||
if (!is_null($configureForRedirect)) {
|
||||
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,14 +580,15 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Filter items that have entities set as a polymorphic relation.
|
||||
* @param Builder|\Illuminate\Database\Query\Builder $query
|
||||
*/
|
||||
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
|
||||
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
|
||||
{
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails, $action) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
$permissionQuery->select(['role_id'])->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
|
||||
->where('action', '=', $action)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Api\ApiToken;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
@@ -240,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
||||
return $this->hasMany(ApiToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the favourite instances for this user.
|
||||
*/
|
||||
public function favourites(): HasMany
|
||||
{
|
||||
return $this->hasMany(Favourite::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last activity time for this user.
|
||||
*/
|
||||
|
||||
@@ -8,13 +8,11 @@ use BookStack\Entities\Models\Chapter;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\UserAvatars;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Images;
|
||||
use Log;
|
||||
|
||||
class UserRepo
|
||||
@@ -184,16 +182,11 @@ class UserRepo
|
||||
{
|
||||
$user->socialAccounts()->delete();
|
||||
$user->apiTokens()->delete();
|
||||
$user->favourites()->delete();
|
||||
$user->delete();
|
||||
|
||||
// Delete user profile images
|
||||
$profileImages = Image::query()->where('type', '=', 'user')
|
||||
->where('uploaded_to', '=', $user->id)
|
||||
->get();
|
||||
|
||||
foreach ($profileImages as $image) {
|
||||
Images::destroy($image);
|
||||
}
|
||||
$this->userAvatar->destroyAllForUser($user);
|
||||
|
||||
if (!empty($newOwnerId)) {
|
||||
$newOwner = User::query()->find($newOwnerId);
|
||||
|
||||
@@ -184,11 +184,8 @@ return [
|
||||
|
||||
// Custom BookStack
|
||||
'Activity' => BookStack\Facades\Activity::class,
|
||||
'Views' => BookStack\Facades\Views::class,
|
||||
'Images' => BookStack\Facades\Images::class,
|
||||
'Permissions' => BookStack\Facades\Permissions::class,
|
||||
'Theme' => BookStack\Facades\Theme::class,
|
||||
|
||||
],
|
||||
|
||||
// Proxy configuration
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
|
||||
* Symbol, ZapfDingbats.
|
||||
*/
|
||||
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
"DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
|
||||
|
||||
/**
|
||||
* The location of the DOMPDF font cache directory
|
||||
@@ -219,7 +219,7 @@ return [
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => true,
|
||||
"DOMPDF_ENABLE_JAVASCRIPT" => false,
|
||||
|
||||
/**
|
||||
* Enable remote file access
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
|
||||
|
||||
return [
|
||||
|
||||
// Display name, shown to users, for SAML2 option
|
||||
@@ -139,6 +141,14 @@ return [
|
||||
// )
|
||||
// ),
|
||||
],
|
||||
'security' => [
|
||||
// SAML2 Authn context
|
||||
// When set to false no AuthContext will be sent in the AuthNRequest,
|
||||
// When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.
|
||||
// Multiple forced values can be passed via a space separated array, For example:
|
||||
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
||||
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -133,6 +133,7 @@ return [
|
||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
|
||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
||||
'start_tls' => env('LDAP_START_TLS', false),
|
||||
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearViews extends Command
|
||||
@@ -36,7 +37,7 @@ class ClearViews extends Command
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
\Views::resetAll();
|
||||
View::clearAll();
|
||||
$this->comment('Views cleared');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use BookStack\Actions\Activity;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\Favourite;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
@@ -9,7 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Entities\Tools\SearchIndex;
|
||||
use BookStack\Entities\Tools\SlugGenerator;
|
||||
use BookStack\Facades\Permissions;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Interfaces\Sluggable;
|
||||
use BookStack\Interfaces\Viewable;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use BookStack\Traits\HasOwner;
|
||||
@@ -38,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static Builder withLastView()
|
||||
* @method static Builder withViewCount()
|
||||
*/
|
||||
abstract class Entity extends Model implements Sluggable
|
||||
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasCreatorAndUpdater;
|
||||
@@ -297,4 +300,22 @@ abstract class Entity extends Model implements Sluggable
|
||||
$this->slug = app(SlugGenerator::class)->generate($this);
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function favourites(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Favourite::class, 'favouritable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the entity is a favourite of the current user.
|
||||
*/
|
||||
public function isFavourite(): bool
|
||||
{
|
||||
return $this->favourites()
|
||||
->where('user_id', '=', user()->id)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +75,23 @@ class Page extends BookChild
|
||||
|
||||
/**
|
||||
* Get the associated page revisions, ordered by created date.
|
||||
* @return mixed
|
||||
* Only provides actual saved page revision instances, Not drafts.
|
||||
*/
|
||||
public function revisions()
|
||||
public function revisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
|
||||
return $this->allRevisions()
|
||||
->where('type', '=', 'version')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all revision instances assigned to this page.
|
||||
* Includes all types of revisions.
|
||||
*/
|
||||
public function allRevisions(): HasMany
|
||||
{
|
||||
return $this->hasMany(PageRevision::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
17
app/Entities/Queries/EntityQuery.php
Normal file
17
app/Entities/Queries/EntityQuery.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
|
||||
abstract class EntityQuery
|
||||
{
|
||||
protected function permissionService(): PermissionService
|
||||
{
|
||||
return app()->make(PermissionService::class);
|
||||
}
|
||||
|
||||
protected function entityProvider(): EntityProvider
|
||||
{
|
||||
return app()->make(EntityProvider::class);
|
||||
}
|
||||
}
|
||||
29
app/Entities/Queries/Popular.php
Normal file
29
app/Entities/Queries/Popular.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Popular extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
|
||||
{
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
|
||||
->groupBy('viewable_id', 'viewable_type')
|
||||
->orderBy('view_count', 'desc');
|
||||
|
||||
if ($filterModels) {
|
||||
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
|
||||
}
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip($count * ($page - 1))
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
|
||||
}
|
||||
32
app/Entities/Queries/RecentlyViewed.php
Normal file
32
app/Entities/Queries/RecentlyViewed.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class RecentlyViewed extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $page): Collection
|
||||
{
|
||||
$user = user();
|
||||
if ($user === null || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()->filterRestrictedEntityRelations(
|
||||
View::query(),
|
||||
'views',
|
||||
'viewable_id',
|
||||
'viewable_type',
|
||||
'view'
|
||||
)
|
||||
->orderBy('views.updated_at', 'desc')
|
||||
->where('user_id', '=', user()->id);
|
||||
|
||||
return $query->with('viewable')
|
||||
->skip(($page - 1) * $count)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('viewable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
33
app/Entities/Queries/TopFavourites.php
Normal file
33
app/Entities/Queries/TopFavourites.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php namespace BookStack\Entities\Queries;
|
||||
|
||||
use BookStack\Actions\Favourite;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
|
||||
class TopFavourites extends EntityQuery
|
||||
{
|
||||
public function run(int $count, int $skip = 0)
|
||||
{
|
||||
$user = user();
|
||||
if (is_null($user) || $user->isDefault()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$query = $this->permissionService()
|
||||
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
|
||||
->select('favourites.*')
|
||||
->leftJoin('views', function (JoinClause $join) {
|
||||
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
|
||||
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
|
||||
$join->where('views.user_id', '=', user()->id);
|
||||
})
|
||||
->orderBy('views.views', 'desc')
|
||||
->where('favourites.user_id', '=', user()->id);
|
||||
|
||||
return $query->with('favouritable')
|
||||
->skip($skip)
|
||||
->take($count)
|
||||
->get()
|
||||
->pluck('favouritable')
|
||||
->filter();
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ class PageRepo
|
||||
if (!empty($input['markdown'] ?? '')) {
|
||||
$pageContent->setNewMarkdown($input['markdown']);
|
||||
} else {
|
||||
$pageContent->setNewHTML($input['html']);
|
||||
$pageContent->setNewHTML($input['html'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
69
app/Entities/Tools/NextPreviousContentLocator.php
Normal file
69
app/Entities/Tools/NextPreviousContentLocator.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php namespace BookStack\Entities\Tools;
|
||||
|
||||
use BookStack\Entities\Models\BookChild;
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Finds the next or previous content of a book element (page or chapter).
|
||||
*/
|
||||
class NextPreviousContentLocator
|
||||
{
|
||||
protected $relativeBookItem;
|
||||
protected $flatTree;
|
||||
protected $currentIndex = null;
|
||||
|
||||
/**
|
||||
* NextPreviousContentLocator constructor.
|
||||
*/
|
||||
public function __construct(BookChild $relativeBookItem, Collection $bookTree)
|
||||
{
|
||||
$this->relativeBookItem = $relativeBookItem;
|
||||
$this->flatTree = $this->treeToFlatOrderedCollection($bookTree);
|
||||
$this->currentIndex = $this->getCurrentIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next logical entity within the book hierarchy.
|
||||
*/
|
||||
public function getNext(): ?Entity
|
||||
{
|
||||
return $this->flatTree->get($this->currentIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next logical entity within the book hierarchy.
|
||||
*/
|
||||
public function getPrevious(): ?Entity
|
||||
{
|
||||
return $this->flatTree->get($this->currentIndex - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of the current relative item.
|
||||
*/
|
||||
protected function getCurrentIndex(): ?int
|
||||
{
|
||||
$index = $this->flatTree->search(function (Entity $entity) {
|
||||
return get_class($entity) === get_class($this->relativeBookItem)
|
||||
&& $entity->id === $this->relativeBookItem->id;
|
||||
});
|
||||
return $index === false ? null : $index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a book tree collection to a flattened version
|
||||
* where all items follow the expected order of user flow.
|
||||
*/
|
||||
protected function treeToFlatOrderedCollection(Collection $bookTree): Collection
|
||||
{
|
||||
$flatOrdered = collect();
|
||||
/** @var Entity $item */
|
||||
foreach ($bookTree->all() as $item) {
|
||||
$flatOrdered->push($item);
|
||||
$childPages = $item->visible_pages ?? [];
|
||||
$flatOrdered = $flatOrdered->concat($childPages);
|
||||
}
|
||||
return $flatOrdered;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use DOMDocument;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
@@ -169,7 +170,7 @@ class PageContent
|
||||
$content = $this->page->html;
|
||||
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($content);
|
||||
$content = HtmlContentFilter::removeScripts($content);
|
||||
}
|
||||
|
||||
if ($blankIncludes) {
|
||||
@@ -308,65 +309,4 @@ class PageContent
|
||||
|
||||
return $innerContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape script tags within HTML content.
|
||||
*/
|
||||
protected function escapeScripts(string $html) : string
|
||||
{
|
||||
if (empty($html)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
// Remove standard script tags
|
||||
$scriptElems = $xPath->query('//script');
|
||||
foreach ($scriptElems as $scriptElem) {
|
||||
$scriptElem->parentNode->removeChild($scriptElem);
|
||||
}
|
||||
|
||||
// Remove clickable links to JavaScript URI
|
||||
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||
foreach ($badLinks as $badLink) {
|
||||
$badLink->parentNode->removeChild($badLink);
|
||||
}
|
||||
|
||||
// Remove forms with calls to JavaScript URI
|
||||
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||
foreach ($badForms as $badForm) {
|
||||
$badForm->parentNode->removeChild($badForm);
|
||||
}
|
||||
|
||||
// Remove meta tag to prevent external redirects
|
||||
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||
foreach ($metaTags as $metaTag) {
|
||||
$metaTag->parentNode->removeChild($metaTag);
|
||||
}
|
||||
|
||||
// Remove data or JavaScript iFrames
|
||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||
foreach ($badIframes as $badIframe) {
|
||||
$badIframe->parentNode->removeChild($badIframe);
|
||||
}
|
||||
|
||||
// Remove 'on*' attributes
|
||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
||||
foreach ($onAttributes as $attr) {
|
||||
/** @var \DOMAttr $attr*/
|
||||
$attrName = $attr->nodeName;
|
||||
$attr->parentNode->removeAttribute($attrName);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||
foreach ($topElems as $child) {
|
||||
$html .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ class TrashCan
|
||||
protected function destroyPage(Page $page): int
|
||||
{
|
||||
$this->destroyCommonRelations($page);
|
||||
$page->allRevisions()->delete();
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
@@ -317,6 +318,7 @@ class TrashCan
|
||||
$entity->jointPermissions()->delete();
|
||||
$entity->searchTerms()->delete();
|
||||
$entity->deletions()->delete();
|
||||
$entity->favourites()->delete();
|
||||
|
||||
if ($entity instanceof HasCoverImage && $entity->cover) {
|
||||
$imageService = app()->make(ImageService::class);
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
@@ -58,29 +57,6 @@ class Handler extends ExceptionHandler
|
||||
return $this->renderApiException($e);
|
||||
}
|
||||
|
||||
// Handle notify exceptions which will redirect to the
|
||||
// specified location then show a notification message.
|
||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
return redirect($e->redirectLocation);
|
||||
}
|
||||
|
||||
// Handle pretty exceptions which will show a friendly application-fitting page
|
||||
// Which will include the basic message to point the user roughly to the cause.
|
||||
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
|
||||
$message = $this->getOriginalMessage($e);
|
||||
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
|
||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||
}
|
||||
|
||||
// Handle 404 errors with a loaded session to enable showing user-specific information
|
||||
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
|
||||
return \Route::respondWithRoute('fallback');
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
@@ -119,30 +95,6 @@ class Handler extends ExceptionHandler
|
||||
return new JsonResponse($responseData, $code, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the exception chain to compare against the original exception type.
|
||||
*/
|
||||
protected function isExceptionType(Exception $e, string $type): bool
|
||||
{
|
||||
do {
|
||||
if (is_a($e, $type)) {
|
||||
return true;
|
||||
}
|
||||
} while ($e = $e->getPrevious());
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original exception message.
|
||||
*/
|
||||
protected function getOriginalMessage(Exception $e): string
|
||||
{
|
||||
do {
|
||||
$message = $e->getMessage();
|
||||
} while ($e = $e->getPrevious());
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an authentication exception into an unauthenticated response.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class NotifyException extends \Exception
|
||||
{
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class NotifyException extends Exception implements Responsable
|
||||
{
|
||||
public $message;
|
||||
public $redirectLocation;
|
||||
|
||||
@@ -15,4 +17,19 @@ class NotifyException extends \Exception
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the response for this type of exception.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
$message = $this->getMessage();
|
||||
|
||||
if (!empty($message)) {
|
||||
session()->flash('error', $message);
|
||||
}
|
||||
|
||||
return redirect($this->redirectLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class PrettyException extends \Exception
|
||||
{
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Support\Responsable;
|
||||
|
||||
class PrettyException extends Exception implements Responsable
|
||||
{
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $subtitle = null;
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
protected $details = null;
|
||||
|
||||
/**
|
||||
* Render a response for when this exception occurs.
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function toResponse($request)
|
||||
{
|
||||
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
|
||||
return response()->view('errors.' . $code, [
|
||||
'message' => $this->getMessage(),
|
||||
'subtitle' => $this->subtitle,
|
||||
'details' => $this->details,
|
||||
], $code);
|
||||
}
|
||||
|
||||
public function setSubtitle(string $subtitle): self
|
||||
{
|
||||
$this->subtitle = $subtitle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDetails(string $details): self
|
||||
{
|
||||
$this->details = $details;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Images extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'images';
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Views extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'views';
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
@@ -198,4 +199,19 @@ class LoginController extends Controller
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the failed login response instance.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function sendFailedLoginResponse(Request $request)
|
||||
{
|
||||
throw ValidationException::withMessages([
|
||||
$this->username() => [trans('auth.failed')],
|
||||
])->redirectTo('/login');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Actions\ActivityType;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Models\Bookshelf;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
@@ -11,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class BookController extends Controller
|
||||
{
|
||||
@@ -112,7 +112,7 @@ class BookController extends Controller
|
||||
$bookChildren = (new BookContents($book))->getTree(true);
|
||||
$bookParentShelves = $book->shelves()->visible()->get();
|
||||
|
||||
Views::add($book);
|
||||
View::incrementFor($book);
|
||||
if ($request->has('shelf')) {
|
||||
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
@@ -109,7 +110,7 @@ class BookshelfController extends Controller
|
||||
->values()
|
||||
->all();
|
||||
|
||||
Views::add($shelf);
|
||||
View::incrementFor($shelf);
|
||||
$this->entityContextManager->setShelfContext($shelf->id);
|
||||
$view = setting()->getForCurrentUser('bookshelf_view_type');
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Repos\ChapterRepo;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\MoveOperationException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class ChapterController extends Controller
|
||||
{
|
||||
@@ -64,7 +65,8 @@ class ChapterController extends Controller
|
||||
|
||||
$sidebarTree = (new BookContents($chapter->book))->getTree();
|
||||
$pages = $chapter->getVisiblePages();
|
||||
Views::add($chapter);
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
|
||||
View::incrementFor($chapter);
|
||||
|
||||
$this->setPageTitle($chapter->getShortName());
|
||||
return view('chapters.show', [
|
||||
@@ -72,7 +74,9 @@ class ChapterController extends Controller
|
||||
'chapter' => $chapter,
|
||||
'current' => $chapter,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'pages' => $pages
|
||||
'pages' => $pages,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
95
app/Http/Controllers/FavouriteController.php
Normal file
95
app/Http/Controllers/FavouriteController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Models\Entity;
|
||||
use BookStack\Entities\Queries\TopFavourites;
|
||||
use BookStack\Interfaces\Favouritable;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FavouriteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Show a listing of all favourite items for the current user.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$viewCount = 20;
|
||||
$page = intval($request->get('page', 1));
|
||||
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||
|
||||
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
|
||||
|
||||
return view('common.detailed-listing-with-more', [
|
||||
'title' => trans('entities.my_favourites'),
|
||||
'entities' => $favourites->slice(0, $viewCount),
|
||||
'hasMoreLink' => $hasMoreLink,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new item as a favourite.
|
||||
*/
|
||||
public function add(Request $request)
|
||||
{
|
||||
$favouritable = $this->getValidatedModelFromRequest($request);
|
||||
$favouritable->favourites()->firstOrCreate([
|
||||
'user_id' => user()->id,
|
||||
]);
|
||||
|
||||
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
|
||||
'name' => $favouritable->name,
|
||||
]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item as a favourite.
|
||||
*/
|
||||
public function remove(Request $request)
|
||||
{
|
||||
$favouritable = $this->getValidatedModelFromRequest($request);
|
||||
$favouritable->favourites()->where([
|
||||
'user_id' => user()->id,
|
||||
])->delete();
|
||||
|
||||
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
|
||||
'name' => $favouritable->name,
|
||||
]));
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getValidatedModelFromRequest(Request $request): Favouritable
|
||||
{
|
||||
$modelInfo = $this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'id' => 'required|integer',
|
||||
]);
|
||||
|
||||
if (!class_exists($modelInfo['type'])) {
|
||||
throw new \Exception('Model not found');
|
||||
}
|
||||
|
||||
/** @var Model $model */
|
||||
$model = new $modelInfo['type'];
|
||||
if (! $model instanceof Favouritable) {
|
||||
throw new \Exception('Model not favouritable');
|
||||
}
|
||||
|
||||
$modelInstance = $model->newQuery()
|
||||
->where('id', '=', $modelInfo['id'])
|
||||
->first(['id', 'name']);
|
||||
|
||||
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
|
||||
if (is_null($modelInstance) || $inaccessibleEntity) {
|
||||
throw new \Exception('Model instance not found');
|
||||
}
|
||||
|
||||
return $modelInstance;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
use Activity;
|
||||
use BookStack\Entities\Models\Book;
|
||||
use BookStack\Entities\Queries\RecentlyViewed;
|
||||
use BookStack\Entities\Queries\TopFavourites;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\BookRepo;
|
||||
use BookStack\Entities\Repos\BookshelfRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class HomeController extends Controller
|
||||
@@ -32,12 +33,13 @@ class HomeController extends Controller
|
||||
|
||||
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
|
||||
$recents = $this->isSignedIn() ?
|
||||
Views::getUserRecentlyViewed(12*$recentFactor, 1)
|
||||
(new RecentlyViewed)->run(12*$recentFactor, 1)
|
||||
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
|
||||
$favourites = (new TopFavourites)->run(6);
|
||||
$recentlyUpdatedPages = Page::visible()->with('book')
|
||||
->where('draft', false)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->take(12)
|
||||
->take($favourites->count() > 0 ? 6 : 12)
|
||||
->get();
|
||||
|
||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||
@@ -51,6 +53,7 @@ class HomeController extends Controller
|
||||
'recents' => $recents,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages,
|
||||
'favourites' => $favourites,
|
||||
];
|
||||
|
||||
// Add required list ordering & sorting for books & shelves views.
|
||||
@@ -105,7 +108,7 @@ class HomeController extends Controller
|
||||
*/
|
||||
public function customHeadContent()
|
||||
{
|
||||
return view('partials.custom-head-content');
|
||||
return view('partials.custom-head');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers\Images;
|
||||
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
@@ -27,12 +28,15 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Provide an image file from storage.
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function showImage(string $path)
|
||||
{
|
||||
$path = storage_path('uploads/images/' . $path);
|
||||
if (!file_exists($path)) {
|
||||
abort(404);
|
||||
throw (new NotFoundException(trans('errors.image_not_found')))
|
||||
->setSubtitle(trans('errors.image_not_found_subtitle'))
|
||||
->setDetails(trans('errors.image_not_found_details'));
|
||||
}
|
||||
|
||||
return response()->file($path);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Entities\Tools\BookContents;
|
||||
use BookStack\Entities\Tools\NextPreviousContentLocator;
|
||||
use BookStack\Entities\Tools\PageContent;
|
||||
use BookStack\Entities\Tools\PageEditActivity;
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Entities\Tools\PermissionsUpdater;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
@@ -142,7 +142,9 @@ class PageController extends Controller
|
||||
$page->load(['comments.createdBy']);
|
||||
}
|
||||
|
||||
Views::add($page);
|
||||
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
|
||||
|
||||
View::incrementFor($page);
|
||||
$this->setPageTitle($page->getShortName());
|
||||
return view('pages.show', [
|
||||
'page' => $page,
|
||||
@@ -150,7 +152,9 @@ class PageController extends Controller
|
||||
'current' => $page,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'commentsEnabled' => $commentsEnabled,
|
||||
'pageNav' => $pageNav
|
||||
'pageNav' => $pageNav,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -243,8 +247,8 @@ class PageController extends Controller
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'status' => 'success',
|
||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||
'timestamp' => $updateTime
|
||||
]);
|
||||
}
|
||||
@@ -267,7 +271,7 @@ class PageController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
|
||||
return view('pages.delete', [
|
||||
'book' => $page->book,
|
||||
'page' => $page,
|
||||
@@ -283,7 +287,7 @@ class PageController extends Controller
|
||||
{
|
||||
$page = $this->pageRepo->getById($pageId);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
|
||||
return view('pages.delete', [
|
||||
'book' => $page->book,
|
||||
'page' => $page,
|
||||
@@ -295,7 +299,6 @@ class PageController extends Controller
|
||||
* Remove the specified page from storage.
|
||||
* @throws NotFoundException
|
||||
* @throws Throwable
|
||||
* @throws NotifyException
|
||||
*/
|
||||
public function destroy(string $bookSlug, string $pageSlug)
|
||||
{
|
||||
@@ -311,7 +314,6 @@ class PageController extends Controller
|
||||
/**
|
||||
* Remove the specified draft page from storage.
|
||||
* @throws NotFoundException
|
||||
* @throws NotifyException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function destroyDraft(string $bookSlug, int $pageId)
|
||||
@@ -340,9 +342,9 @@ class PageController extends Controller
|
||||
->paginate(20)
|
||||
->setPath(url('/pages/recently-updated'));
|
||||
|
||||
return view('pages.detailed-listing', [
|
||||
return view('common.detailed-listing-paginated', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'pages' => $pages
|
||||
'entities' => $pages
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -380,7 +382,7 @@ class PageController extends Controller
|
||||
try {
|
||||
$parent = $this->pageRepo->move($page, $entitySelection);
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
@@ -424,7 +426,7 @@ class PageController extends Controller
|
||||
try {
|
||||
$pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
|
||||
} catch (Exception $exception) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
if ($exception instanceof PermissionsException) {
|
||||
$this->showPermissionError();
|
||||
}
|
||||
|
||||
@@ -445,7 +447,7 @@ class PageController extends Controller
|
||||
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
return view('pages.permissions', [
|
||||
'page' => $page,
|
||||
'page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Entities\Queries\Popular;
|
||||
use BookStack\Entities\Tools\SearchRunner;
|
||||
use BookStack\Entities\Tools\ShelfContext;
|
||||
use BookStack\Entities\Tools\SearchOptions;
|
||||
@@ -9,16 +9,13 @@ use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
protected $viewService;
|
||||
protected $searchRunner;
|
||||
protected $entityContextManager;
|
||||
|
||||
public function __construct(
|
||||
ViewService $viewService,
|
||||
SearchRunner $searchRunner,
|
||||
ShelfContext $entityContextManager
|
||||
) {
|
||||
$this->viewService = $viewService;
|
||||
$this->searchRunner = $searchRunner;
|
||||
$this->entityContextManager = $entityContextManager;
|
||||
}
|
||||
@@ -82,7 +79,7 @@ class SearchController extends Controller
|
||||
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
||||
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
|
||||
} else {
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
||||
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
|
||||
}
|
||||
|
||||
return view('search.entity-ajax-list', ['entities' => $entities]);
|
||||
|
||||
11
app/Interfaces/Favouritable.php
Normal file
11
app/Interfaces/Favouritable.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
interface Favouritable
|
||||
{
|
||||
/**
|
||||
* Get the related favourite instances.
|
||||
*/
|
||||
public function favourites(): MorphMany;
|
||||
}
|
||||
11
app/Interfaces/Viewable.php
Normal file
11
app/Interfaces/Viewable.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php namespace BookStack\Interfaces;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
interface Viewable
|
||||
{
|
||||
/**
|
||||
* Get all view instances for this viewable model.
|
||||
*/
|
||||
public function views(): MorphMany;
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Actions\ActivityService;
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Theming\ThemeService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
@@ -32,10 +31,6 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
return $this->app->make(ActivityService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('views', function () {
|
||||
return $this->app->make(ViewService::class);
|
||||
});
|
||||
|
||||
$this->app->singleton('images', function () {
|
||||
return $this->app->make(ImageService::class);
|
||||
});
|
||||
|
||||
@@ -53,9 +53,9 @@ class ThemeService
|
||||
/**
|
||||
* @see SocialAuthService::addSocialDriver
|
||||
*/
|
||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
|
||||
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null)
|
||||
{
|
||||
$socialAuthService = app()->make(SocialAuthService::class);
|
||||
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler);
|
||||
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use Images;
|
||||
|
||||
class Image extends Model
|
||||
{
|
||||
@@ -14,23 +13,18 @@ class Image extends Model
|
||||
|
||||
/**
|
||||
* Get a thumbnail for this image.
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param bool|false $keepRatio
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getThumb($width, $height, $keepRatio = false)
|
||||
public function getThumb(int $width, int $height, bool $keepRatio = false): string
|
||||
{
|
||||
return Images::getThumbnail($this, $width, $height, $keepRatio);
|
||||
return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the page this image has been uploaded to.
|
||||
* Only applicable to gallery or drawio image types.
|
||||
* @return Page|null
|
||||
*/
|
||||
public function getPage()
|
||||
public function getPage(): ?Page
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'uploaded_to')->first();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
@@ -106,8 +107,7 @@ class ImageService
|
||||
}
|
||||
|
||||
try {
|
||||
$storage->put($fullPath, $imageData);
|
||||
$storage->setVisibility($fullPath, 'public');
|
||||
$this->saveImageDataInPublicSpace($storage, $fullPath, $imageData);
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Error when attempting image upload:' . $e->getMessage());
|
||||
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
|
||||
@@ -132,6 +132,25 @@ class ImageService
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image data for the given path in the public space, if possible,
|
||||
* for the provided storage mechanism.
|
||||
*/
|
||||
protected function saveImageDataInPublicSpace(Storage $storage, string $path, string $data)
|
||||
{
|
||||
$storage->put($path, $data);
|
||||
|
||||
// Set visibility when a non-AWS-s3, s3-like storage option is in use.
|
||||
// Done since this call can break s3-like services but desired for other image stores.
|
||||
// Attempting to set ACL during above put request requires different permissions
|
||||
// hence would technically be a breaking change for actual s3 usage.
|
||||
$usingS3 = strtolower(config('filesystems.images')) === 's3';
|
||||
$usingS3Like = $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
|
||||
if (!$usingS3Like) {
|
||||
$storage->setVisibility($path, 'public');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up an image file name to be both URL and storage safe.
|
||||
*/
|
||||
@@ -191,8 +210,7 @@ class ImageService
|
||||
|
||||
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
|
||||
|
||||
$storage->put($thumbFilePath, $thumbData);
|
||||
$storage->setVisibility($thumbFilePath, 'public');
|
||||
$this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData);
|
||||
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class UserAvatars
|
||||
}
|
||||
|
||||
try {
|
||||
$this->destroyAllForUser($user);
|
||||
$avatar = $this->saveAvatarImage($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
@@ -34,6 +35,35 @@ class UserAvatars
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a new avatar image to the given user using the given image data.
|
||||
*/
|
||||
public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void
|
||||
{
|
||||
try {
|
||||
$this->destroyAllForUser($user);
|
||||
$avatar = $this->createAvatarImageFromData($user, $imageData, $extension);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to save user avatar image');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all user avatars uploaded to the given user.
|
||||
*/
|
||||
public function destroyAllForUser(User $user)
|
||||
{
|
||||
$profileImages = Image::query()->where('type', '=', 'user')
|
||||
->where('uploaded_to', '=', $user->id)
|
||||
->get();
|
||||
|
||||
foreach ($profileImages as $image) {
|
||||
$this->imageService->destroy($image);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an avatar image from an external service.
|
||||
* @throws Exception
|
||||
@@ -50,8 +80,16 @@ class UserAvatars
|
||||
];
|
||||
|
||||
$userAvatarUrl = strtr($avatarUrl, $replacements);
|
||||
$imageName = str_replace(' ', '-', $user->id . '-avatar.png');
|
||||
$imageData = $this->getAvatarImageData($userAvatarUrl);
|
||||
return $this->createAvatarImageFromData($user, $imageData, 'png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new image instance and saves it in the system as a new user avatar image.
|
||||
*/
|
||||
protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
|
||||
{
|
||||
$imageName = str_replace(' ', '-', $user->id . '-avatar.' . $extension);
|
||||
|
||||
$image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
|
||||
$image->created_by = $user->id;
|
||||
|
||||
71
app/Util/HtmlContentFilter.php
Normal file
71
app/Util/HtmlContentFilter.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php namespace BookStack\Util;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
|
||||
class HtmlContentFilter
|
||||
{
|
||||
/**
|
||||
* Remove all of the script elements from the given HTML.
|
||||
*/
|
||||
public static function removeScripts(string $html): string
|
||||
{
|
||||
if (empty($html)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
|
||||
// Remove standard script tags
|
||||
$scriptElems = $xPath->query('//script');
|
||||
static::removeNodes($scriptElems);
|
||||
|
||||
// Remove clickable links to JavaScript URI
|
||||
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
|
||||
static::removeNodes($badLinks);
|
||||
|
||||
// Remove forms with calls to JavaScript URI
|
||||
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
|
||||
static::removeNodes($badForms);
|
||||
|
||||
// Remove meta tag to prevent external redirects
|
||||
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
|
||||
static::removeNodes($metaTags);
|
||||
|
||||
// Remove data or JavaScript iFrames
|
||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
||||
static::removeNodes($badIframes);
|
||||
|
||||
// Remove 'on*' attributes
|
||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
||||
foreach ($onAttributes as $attr) {
|
||||
/** @var \DOMAttr $attr*/
|
||||
$attrName = $attr->nodeName;
|
||||
$attr->parentNode->removeAttribute($attrName);
|
||||
}
|
||||
|
||||
$html = '';
|
||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
||||
foreach ($topElems as $child) {
|
||||
$html .= $doc->saveHTML($child);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removed all of the given DOMNodes.
|
||||
*/
|
||||
static protected function removeNodes(DOMNodeList $nodes): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
557
composer.lock
generated
557
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ class CreateSearchIndexTable extends Migration
|
||||
{
|
||||
Schema::create('search_terms', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('term', 200);
|
||||
$table->string('term', 180);
|
||||
$table->string('entity_type', 100);
|
||||
$table->integer('entity_id');
|
||||
$table->integer('score');
|
||||
|
||||
@@ -14,7 +14,7 @@ class AddRoleExternalAuthId extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->string('external_auth_id', 200)->default('');
|
||||
$table->string('external_auth_id', 180)->default('');
|
||||
$table->index('external_auth_id');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ class CreateBookshelvesTable extends Migration
|
||||
|
||||
Schema::create('bookshelves', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name', 200);
|
||||
$table->string('slug', 200);
|
||||
$table->string('name', 180);
|
||||
$table->string('slug', 180);
|
||||
$table->text('description');
|
||||
$table->integer('created_by')->nullable()->default(null);
|
||||
$table->integer('updated_by')->nullable()->default(null);
|
||||
|
||||
@@ -15,7 +15,7 @@ class AddUserSlug extends Migration
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->string('slug', 250);
|
||||
$table->string('slug', 180);
|
||||
});
|
||||
|
||||
$slugMap = [];
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateFavouritesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('favourites', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->integer('user_id')->index();
|
||||
$table->integer('favouritable_id');
|
||||
$table->string('favouritable_type', 100);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['favouritable_id', 'favouritable_type'], 'favouritable_index');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('favourites');
|
||||
}
|
||||
}
|
||||
@@ -95,4 +95,18 @@ Theme::listen(ThemeEvents::APP_BOOT, function($app) {
|
||||
'name' => 'Reddit',
|
||||
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle');
|
||||
});
|
||||
```
|
||||
|
||||
In some cases you may need to customize the driver before it performs a redirect.
|
||||
This can be done by providing a callback as a fourth parameter like so:
|
||||
|
||||
```php
|
||||
Theme::addSocialDriver('reddit', [
|
||||
'client_id' => 'abc123',
|
||||
'client_secret' => 'def456789',
|
||||
'name' => 'Reddit',
|
||||
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle', function($driver) {
|
||||
$driver->with(['prompt' => 'select_account']);
|
||||
$driver->scopes(['open_id']);
|
||||
});
|
||||
```
|
||||
1344
package-lock.json
generated
1344
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,17 +16,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^2.1.0",
|
||||
"esbuild": "0.7.8",
|
||||
"esbuild": "0.12.5",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.32.8"
|
||||
"sass": "^1.34.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.60.0",
|
||||
"codemirror": "^5.61.1",
|
||||
"dropzone": "^5.9.2",
|
||||
"markdown-it": "^11.0.1",
|
||||
"markdown-it": "^12.0.6",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"sortablejs": "^1.13.0"
|
||||
}
|
||||
|
||||
97
public/dist/app.js
vendored
97
public/dist/app.js
vendored
File diff suppressed because one or more lines are too long
2
public/dist/export-styles.css
vendored
2
public/dist/export-styles.css
vendored
File diff suppressed because one or more lines are too long
2
public/dist/styles.css
vendored
2
public/dist/styles.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,4 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 265 B |
1
resources/icons/star-outline.svg
Normal file
1
resources/icons/star-outline.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
@@ -1,4 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 258 B After Width: | Height: | Size: 361 B |
@@ -14,6 +14,7 @@ class MarkdownEditor {
|
||||
this.pageId = this.$opts.pageId;
|
||||
this.textDirection = this.$opts.textDirection;
|
||||
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
|
||||
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
|
||||
|
||||
this.markdown = new MarkdownIt({html: true});
|
||||
this.markdown.use(mdTasksLists, {label: true});
|
||||
@@ -446,8 +447,7 @@ class MarkdownEditor {
|
||||
this.insertDrawing(resp.data, cursorPos);
|
||||
DrawIO.close();
|
||||
}).catch(err => {
|
||||
window.$events.emit('error', trans('errors.image_upload_error'));
|
||||
console.log(err);
|
||||
this.handleDrawingUploadError(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -491,12 +491,20 @@ class MarkdownEditor {
|
||||
this.cm.focus();
|
||||
DrawIO.close();
|
||||
}).catch(err => {
|
||||
window.$events.emit('error', this.imageUploadErrorText);
|
||||
console.log(err);
|
||||
this.handleDrawingUploadError(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleDrawingUploadError(error) {
|
||||
if (error.status === 413) {
|
||||
window.$events.emit('error', this.serverUploadLimitText);
|
||||
} else {
|
||||
window.$events.emit('error', this.imageUploadErrorText);
|
||||
}
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
// Make the editor full screen
|
||||
actionFullScreen() {
|
||||
const alreadyFullscreen = this.elem.classList.contains('fullscreen');
|
||||
|
||||
@@ -283,6 +283,15 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
|
||||
const id = "image-" + Math.random().toString(16).slice(2);
|
||||
const loadingImage = window.baseUrl('/loading.gif');
|
||||
|
||||
const handleUploadError = (error) => {
|
||||
if (error.status === 413) {
|
||||
window.$events.emit('error', wysiwygComponent.serverUploadLimitText);
|
||||
} else {
|
||||
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
|
||||
}
|
||||
console.log(error);
|
||||
};
|
||||
|
||||
// Handle updating an existing image
|
||||
if (currentNode) {
|
||||
DrawIO.close();
|
||||
@@ -292,8 +301,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
|
||||
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
|
||||
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
|
||||
} catch (err) {
|
||||
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
|
||||
console.log(err);
|
||||
handleUploadError(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -307,8 +315,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
|
||||
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
|
||||
} catch (err) {
|
||||
pageEditor.dom.remove(id);
|
||||
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
|
||||
console.log(err);
|
||||
handleUploadError(err);
|
||||
}
|
||||
}, 5);
|
||||
}
|
||||
@@ -432,6 +439,7 @@ class WysiwygEditor {
|
||||
this.pageId = this.$opts.pageId;
|
||||
this.textDirection = this.$opts.textDirection;
|
||||
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
|
||||
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
|
||||
this.isDarkMode = document.documentElement.classList.contains('dark-mode');
|
||||
|
||||
this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
let iFrame = null;
|
||||
|
||||
let lastApprovedOrigin;
|
||||
let onInit, onSave;
|
||||
|
||||
/**
|
||||
@@ -19,15 +19,22 @@ function show(drawioUrl, onInitCallback, onSaveCallback) {
|
||||
iFrame.setAttribute('class', 'fullscreen');
|
||||
iFrame.style.backgroundColor = '#FFFFFF';
|
||||
document.body.appendChild(iFrame);
|
||||
lastApprovedOrigin = (new URL(drawioUrl)).origin;
|
||||
}
|
||||
|
||||
function close() {
|
||||
drawEventClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive and handle a message event from the draw.io window.
|
||||
* @param {MessageEvent} event
|
||||
*/
|
||||
function drawReceive(event) {
|
||||
if (!event.data || event.data.length < 1) return;
|
||||
let message = JSON.parse(event.data);
|
||||
if (event.origin !== lastApprovedOrigin) return;
|
||||
|
||||
const message = JSON.parse(event.data);
|
||||
if (message.event === 'init') {
|
||||
drawEventInit();
|
||||
} else if (message.event === 'exit') {
|
||||
@@ -62,7 +69,7 @@ function drawEventClose() {
|
||||
}
|
||||
|
||||
function drawPostMessage(data) {
|
||||
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
|
||||
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
|
||||
}
|
||||
|
||||
async function upload(imageData, pageUploadedToId) {
|
||||
|
||||
@@ -33,7 +33,7 @@ return [
|
||||
'book_delete' => 'تم حذف الكتاب',
|
||||
'book_delete_notification' => 'تم حذف الكتاب بنجاح',
|
||||
'book_sort' => 'تم سرد الكتاب',
|
||||
'book_sort_notification' => 'تمت إعادة سرد الكتاب بنجاح',
|
||||
'book_sort_notification' => 'أُعِيدَ سرد الكتاب بنجاح',
|
||||
|
||||
// Bookshelves
|
||||
'bookshelf_create' => 'تم إنشاء رف الكتب',
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'تم تحديث الرف',
|
||||
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'تم التعليق',
|
||||
'permissions_update' => 'تحديث الأذونات',
|
||||
|
||||
@@ -47,7 +47,7 @@ return [
|
||||
'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.',
|
||||
'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName',
|
||||
'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.',
|
||||
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم, فلا حاجة لاتخاذ أية خطوات.',
|
||||
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم، فلا حاجة لاتخاذ أية خطوات.',
|
||||
|
||||
|
||||
// Email Confirmation
|
||||
@@ -62,11 +62,11 @@ return [
|
||||
'email_not_confirmed' => 'لم يتم تأكيد البريد الإلكتروني',
|
||||
'email_not_confirmed_text' => 'لم يتم بعد تأكيد عنوان البريد الإلكتروني.',
|
||||
'email_not_confirmed_click_link' => 'الرجاء الضغط على الرابط المرسل إلى بريدكم الإلكتروني بعد تسجيلكم.',
|
||||
'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة, بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',
|
||||
'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة، بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',
|
||||
'email_not_confirmed_resend_button' => 'إعادة إرسال رسالة التأكيد',
|
||||
|
||||
// User Invite
|
||||
'user_invite_email_subject' => 'تم دعوتك للإنضمام إلى صفحة الحالة الخاصة بـ :app_name!',
|
||||
'user_invite_email_subject' => 'تمت دعوتك للانضمام إلى صفحة الحالة الخاصة بـ :app_name!',
|
||||
'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على %site%.',
|
||||
'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة مرور الحساب والحصول على الوصول:',
|
||||
'user_invite_email_action' => 'كلمة سر المستخدم',
|
||||
|
||||
@@ -40,22 +40,26 @@ return [
|
||||
'remove' => 'إزالة',
|
||||
'add' => 'إضافة',
|
||||
'fullscreen' => 'شاشة كاملة',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'خيارات الترتيب',
|
||||
'sort_direction_toggle' => 'الترتيب وفق الإتجاه',
|
||||
'sort_options' => 'خيارات الفرز',
|
||||
'sort_direction_toggle' => 'الفرز وفق الاتجاه',
|
||||
'sort_ascending' => 'فرز تصاعدي',
|
||||
'sort_descending' => 'فرز تنازلي',
|
||||
'sort_name' => 'الاسم',
|
||||
'sort_default' => 'Default',
|
||||
'sort_default' => 'افتراضي',
|
||||
'sort_created_at' => 'تاريخ الإنشاء',
|
||||
'sort_updated_at' => 'تاريخ التحديث',
|
||||
|
||||
// Misc
|
||||
'deleted_user' => 'حذف مستخدم',
|
||||
'deleted_user' => 'المستخدم المحذوف',
|
||||
'no_activity' => 'لا يوجد نشاط لعرضه',
|
||||
'no_items' => 'لا توجد عناصر متوفرة',
|
||||
'back_to_top' => 'العودة للبداية',
|
||||
'back_to_top' => 'العودة إلى الأعلى',
|
||||
'toggle_details' => 'عرض / إخفاء التفاصيل',
|
||||
'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة',
|
||||
'details' => 'التفاصيل',
|
||||
@@ -65,7 +69,7 @@ return [
|
||||
'breadcrumb' => 'شريط التنقل',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Expand Header Menu',
|
||||
'header_menu_expand' => 'عرض القائمة',
|
||||
'profile_menu' => 'قائمة ملف التعريف',
|
||||
'view_profile' => 'عرض الملف الشخصي',
|
||||
'edit_profile' => 'تعديل الملف الشخصي',
|
||||
@@ -74,16 +78,16 @@ return [
|
||||
|
||||
// Layout tabs
|
||||
'tab_info' => 'معلومات',
|
||||
'tab_info_label' => 'Tab: Show Secondary Information',
|
||||
'tab_info_label' => 'تبويب: إظهار المعلومات الثانوية',
|
||||
'tab_content' => 'المحتوى',
|
||||
'tab_content_label' => 'Tab: Show Primary Content',
|
||||
'tab_content_label' => 'تبويب: إظهار المحتوى الأساسي',
|
||||
|
||||
// Email Content
|
||||
'email_action_help' => 'إذا واجهتكم مشكلة بضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
|
||||
'email_action_help' => 'إذا واجهتكم مشكلة عند ضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
|
||||
'email_rights' => 'جميع الحقوق محفوظة',
|
||||
|
||||
// Footer Link Options
|
||||
// Not directly used but available for convenience to users.
|
||||
'privacy_policy' => 'Privacy Policy',
|
||||
'terms_of_service' => 'Terms of Service',
|
||||
'privacy_policy' => 'سياسة الخصوصية',
|
||||
'terms_of_service' => 'اتفاقية شروط الخدمة',
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
'image_load_more' => 'المزيد',
|
||||
'image_image_name' => 'اسم الصورة',
|
||||
'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
|
||||
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة ؟',
|
||||
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',
|
||||
'image_select_image' => 'تحديد الصورة',
|
||||
'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
|
||||
'images_deleted' => 'تم حذف الصور',
|
||||
|
||||
@@ -11,7 +11,7 @@ return [
|
||||
'recently_updated_pages' => 'صفحات حُدثت مؤخراً',
|
||||
'recently_created_chapters' => 'فصول أنشئت مؤخراً',
|
||||
'recently_created_books' => 'كتب أنشئت مؤخراً',
|
||||
'recently_created_shelves' => 'الأرفف المنشأة مؤخراً',
|
||||
'recently_created_shelves' => 'أرفف أنشئت مؤخراً',
|
||||
'recently_update' => 'حُدثت مؤخراً',
|
||||
'recently_viewed' => 'عُرضت مؤخراً',
|
||||
'recent_activity' => 'نشاطات حديثة',
|
||||
@@ -27,9 +27,11 @@ return [
|
||||
'images' => 'صور',
|
||||
'my_recent_drafts' => 'مسوداتي الحديثة',
|
||||
'my_recently_viewed' => 'ما عرضته مؤخراً',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'لم تستعرض أي صفحات',
|
||||
'no_pages_recently_created' => 'لم يتم إنشاء أي صفحات مؤخراً',
|
||||
'no_pages_recently_updated' => 'لم يتم تحديث أي صفحات مؤخراً',
|
||||
'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',
|
||||
'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',
|
||||
'export' => 'تصدير',
|
||||
'export_html' => 'صفحة ويب',
|
||||
'export_pdf' => 'ملف PDF',
|
||||
@@ -37,7 +39,7 @@ return [
|
||||
|
||||
// Permissions and restrictions
|
||||
'permissions' => 'الأذونات',
|
||||
'permissions_intro' => 'في حال التفعيل, ستتم تبدية هذه الأذونات على أذونات الأدوار.',
|
||||
'permissions_intro' => 'عند التفعيل، سوف تأخذ هذه الأذونات أولوية على أي صلاحية أخرى للدور.',
|
||||
'permissions_enable' => 'تفعيل الأذونات المخصصة',
|
||||
'permissions_save' => 'حفظ الأذونات',
|
||||
'permissions_owner' => 'Owner',
|
||||
@@ -55,8 +57,8 @@ return [
|
||||
'search_exact_matches' => 'نتائج مطابقة تماماً',
|
||||
'search_tags' => 'بحث الوسوم',
|
||||
'search_options' => 'الخيارات',
|
||||
'search_viewed_by_me' => 'تم استعراضها من قبلي',
|
||||
'search_not_viewed_by_me' => 'لم يتم استعراضها من قبلي',
|
||||
'search_viewed_by_me' => 'استعرضت من قبلي',
|
||||
'search_not_viewed_by_me' => 'لم تستعرض من قبلي',
|
||||
'search_permissions_set' => 'حزمة الأذونات',
|
||||
'search_created_by_me' => 'أنشئت بواسطتي',
|
||||
'search_updated_by_me' => 'حُدثت بواسطتي',
|
||||
@@ -74,24 +76,24 @@ return [
|
||||
'shelves' => 'الأرفف',
|
||||
'x_shelves' => ':count رف|:count أرفف',
|
||||
'shelves_long' => 'أرفف الكتب',
|
||||
'shelves_empty' => 'لم يتم إنشاء أي أرفف',
|
||||
'shelves_empty' => 'لم ينشأ أي رف',
|
||||
'shelves_create' => 'إنشاء رف جديد',
|
||||
'shelves_popular' => 'أرفف شعبية',
|
||||
'shelves_popular' => 'أرفف رائجة',
|
||||
'shelves_new' => 'أرفف جديدة',
|
||||
'shelves_new_action' => 'رف جديد',
|
||||
'shelves_popular_empty' => 'ستظهر هنا الأرفف الأكثر رواجًا.',
|
||||
'shelves_new_empty' => 'ستظهر هنا الأرفف التي تم إنشاؤها مؤخرًا.',
|
||||
'shelves_new_empty' => 'ستظهر هنا الأرفف التي أنشئت مؤخرًا.',
|
||||
'shelves_save' => 'حفظ الرف',
|
||||
'shelves_books' => 'كتب على هذا الرف',
|
||||
'shelves_add_books' => 'إضافة كتب لهذا الرف',
|
||||
'shelves_drag_books' => 'اسحب الكتب هنا لإضافتها لهذا الرف',
|
||||
'shelves_drag_books' => 'اسحب الكتب هنا لإضافتها في هذا الرف',
|
||||
'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
|
||||
'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
|
||||
'shelves_edit_named' => 'تحرير رف الكتب: الاسم',
|
||||
'shelves_edit_named' => 'تحرير رف الكتب :name',
|
||||
'shelves_edit' => 'تحرير رف الكتب',
|
||||
'shelves_delete' => 'حذف رف الكتب',
|
||||
'shelves_delete_named' => 'حذف رف الكتب: الاسم',
|
||||
'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب مع الاسم ':المُسمى به'. لن يتم حذف الكتب المتضمنة.",
|
||||
'shelves_delete_named' => 'حذف رف الكتب :name',
|
||||
'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب المسمى ':name'، ولن تحذف الكتب المتضمنة فيه.",
|
||||
'shelves_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الرف؟',
|
||||
'shelves_permissions' => 'أذونات رف الكتب',
|
||||
'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب',
|
||||
@@ -99,7 +101,7 @@ return [
|
||||
'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
|
||||
'shelves_copy_permissions' => 'نسخ الأذونات',
|
||||
'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.',
|
||||
'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى: عد الكتب',
|
||||
'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى :count books',
|
||||
|
||||
// Books
|
||||
'book' => 'كتاب',
|
||||
@@ -115,7 +117,7 @@ return [
|
||||
'books_create' => 'إنشاء كتاب جديد',
|
||||
'books_delete' => 'حذف الكتاب',
|
||||
'books_delete_named' => 'حذف كتاب :bookName',
|
||||
'books_delete_explain' => 'سيتم حذف كتاب \':bookName\'. ستتم إزالة جميع الفصول والصفحات.',
|
||||
'books_delete_explain' => 'سيتم حذف كتاب \':bookName\'، وأيضا حذف جميع الفصول والصفحات.',
|
||||
'books_delete_confirmation' => 'تأكيد حذف الكتاب؟',
|
||||
'books_edit' => 'تعديل الكتاب',
|
||||
'books_edit_named' => 'تعديل كتاب :bookName',
|
||||
@@ -215,7 +217,7 @@ return [
|
||||
'pages_revisions_created_by' => 'أنشئ بواسطة',
|
||||
'pages_revisions_date' => 'تاريخ المراجعة',
|
||||
'pages_revisions_number' => '#',
|
||||
'pages_revisions_numbered' => 'مراجعة #: رقم تعريفي',
|
||||
'pages_revisions_numbered' => 'مراجعة #:id',
|
||||
'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
|
||||
'pages_revisions_changelog' => 'سجل التعديل',
|
||||
'pages_revisions_changes' => 'التعديلات',
|
||||
@@ -228,7 +230,7 @@ return [
|
||||
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
|
||||
'pages_initial_revision' => 'نشر مبدئي',
|
||||
'pages_initial_name' => 'صفحة جديدة',
|
||||
'pages_editing_draft_notification' => 'جار تعديل مسودة لم يتم حفظها من :timeDiff.',
|
||||
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
|
||||
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
|
||||
'pages_draft_edit_active' => [
|
||||
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
|
||||
@@ -237,7 +239,7 @@ return [
|
||||
'time_b' => 'في آخر :minCount دقيقة/دقائق',
|
||||
'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',
|
||||
],
|
||||
'pages_draft_discarded' => 'تم التخلص من المسودة. تم تحديث المحرر بمحتوى الصفحة الحالي',
|
||||
'pages_draft_discarded' => 'تم التخلص من المسودة وتحديث المحرر بمحتوى الصفحة الحالي',
|
||||
'pages_specific' => 'صفحة محددة',
|
||||
'pages_is_template' => 'قالب الصفحة',
|
||||
|
||||
@@ -255,14 +257,14 @@ return [
|
||||
'tags_remove' => 'إزالة هذه العلامة',
|
||||
'attachments' => 'المرفقات',
|
||||
'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
|
||||
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا بلحظتها',
|
||||
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',
|
||||
'attachments_items' => 'العناصر المرفقة',
|
||||
'attachments_upload' => 'رفع ملف',
|
||||
'attachments_link' => 'إرفاق رابط',
|
||||
'attachments_set_link' => 'تحديد الرابط',
|
||||
'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
|
||||
'attachments_dropzone' => 'أسقط الملفات أو اضغط هنا لإرفاق ملف',
|
||||
'attachments_no_files' => 'لم يتم رفع أي ملفات',
|
||||
'attachments_no_files' => 'لم تُرفع أي ملفات',
|
||||
'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
|
||||
'attachments_link_name' => 'اسم الرابط',
|
||||
'attachment_link' => 'رابط المرفق',
|
||||
@@ -287,7 +289,7 @@ return [
|
||||
'templates_prepend_content' => 'بادئة محتوى الصفحة',
|
||||
|
||||
// Profile View
|
||||
'profile_user_for_x' => 'المستخدم لـ : الوقت',
|
||||
'profile_user_for_x' => 'المستخدم لـ :time',
|
||||
'profile_created_content' => 'المحتوى المنشأ',
|
||||
'profile_not_created_pages' => 'لم يتم إنشاء أي صفحات بواسطة :userName',
|
||||
'profile_not_created_chapters' => 'لم يتم إنشاء أي فصول بواسطة :userName',
|
||||
@@ -299,7 +301,7 @@ return [
|
||||
'comments' => 'تعليقات',
|
||||
'comment_add' => 'إضافة تعليق',
|
||||
'comment_placeholder' => 'ضع تعليقاً هنا',
|
||||
'comment_count' => '{0} ا توجد تعليقات|{1} تعليق واحد|{2} تعليقان|[3,*] :count تعليقات',
|
||||
'comment_count' => '{0} لا توجد تعليقات|{1} تعليق واحد|{2} تعليقان[3,*] :count تعليقات',
|
||||
'comment_save' => 'حفظ التعليق',
|
||||
'comment_saving' => 'جار حفظ التعليق...',
|
||||
'comment_deleting' => 'جار حذف التعليق...',
|
||||
@@ -313,8 +315,8 @@ return [
|
||||
'comment_in_reply_to' => 'رداً على :commentId',
|
||||
|
||||
// Revision
|
||||
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذا الإصدار؟',
|
||||
'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذا الإصدار؟ سيتم استبدال محتوى الصفحة الحالية.',
|
||||
'revision_delete_success' => 'تم حذف الإصدار',
|
||||
'revision_cannot_delete_latest' => 'لايمكن حذف آخر إصدار.'
|
||||
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟',
|
||||
'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذه المراجعة؟ سيتم استبدال محتوى الصفحة الحالية.',
|
||||
'revision_delete_success' => 'تم حذف المراجعة',
|
||||
'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.'
|
||||
];
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'لم يتم العثور على الصفحة',
|
||||
'sorry_page_not_found' => 'عفواً, لا يمكن العثور على الصفحة التي تبحث عنها.',
|
||||
'sorry_page_not_found_permission_warning' => 'إذا كنت تتوقع أن تكون هذه الصفحة موجودة، قد لا يكون لديك تصريح بمشاهدتها.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'العودة للصفحة الرئيسية',
|
||||
'error_occurred' => 'حدث خطأ',
|
||||
'app_down' => ':appName لا يعمل حالياً',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'изтрит рафт',
|
||||
'bookshelf_delete_notification' => 'Рафтът беше успешно изтрит',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'коментирано на',
|
||||
'permissions_update' => 'updated permissions',
|
||||
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Премахване',
|
||||
'add' => 'Добави',
|
||||
'fullscreen' => 'Пълен екран',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Опции за сортиране',
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Изображения',
|
||||
'my_recent_drafts' => 'Моите скорошни драфтове',
|
||||
'my_recently_viewed' => 'Моите скорошни преглеждания',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'Не сте прегледали никакви страници',
|
||||
'no_pages_recently_created' => 'Не са били създавани страници скоро',
|
||||
'no_pages_recently_updated' => 'Не са били актуализирани страници скоро',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'Страницата не е намерена',
|
||||
'sorry_page_not_found' => 'Страницата, която търсите не може да бъде намерена.',
|
||||
'sorry_page_not_found_permission_warning' => 'Ако смятате, че тази страница съществува, най-вероятно нямате право да я преглеждате.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'Назад към Начало',
|
||||
'error_occurred' => 'Възникна грешка',
|
||||
'app_down' => ':appName не е достъпно в момента',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
|
||||
'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'je komentarisao/la na',
|
||||
'permissions_update' => 'je ažurirao/la dozvole',
|
||||
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Ukloni',
|
||||
'add' => 'Dodaj',
|
||||
'fullscreen' => 'Prikaz preko čitavog ekrana',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Opcije sortiranja',
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Slike',
|
||||
'my_recent_drafts' => 'Moje nedavne skice',
|
||||
'my_recently_viewed' => 'Moji nedavni pregledi',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'Niste pogledali nijednu stranicu',
|
||||
'no_pages_recently_created' => 'Nijedna stranica nije napravljena nedavno',
|
||||
'no_pages_recently_updated' => 'Niijedna stranica nije ažurirana nedavno',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'Stranica nije pronađena',
|
||||
'sorry_page_not_found' => 'Stranica koju ste tražili nije pronađena.',
|
||||
'sorry_page_not_found_permission_warning' => 'Ako ste očekivali da ova stranica postoji, možda nemate privilegije da joj pristupite.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'Nazad na početnu stranu',
|
||||
'error_occurred' => 'Desila se greška',
|
||||
'app_down' => ':appName trenutno nije u funkciji',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'ha suprimit un prestatge',
|
||||
'bookshelf_delete_notification' => 'Prestatge suprimit correctament',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'ha comentat a',
|
||||
'permissions_update' => 'ha actualitzat els permisos',
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
return [
|
||||
|
||||
'failed' => 'Les credencials no coincideixen amb les que tenim emmagatzemades.',
|
||||
'throttle' => 'Massa intents d\'iniciar la sessió. Torneu-ho a provar d\'aquí a :seconds segons.',
|
||||
'failed' => 'Les credencials no coincideixen amb les que hi ha emmagatzemades.',
|
||||
'throttle' => 'Massa intents d\'inici de sessió. Torna-ho a provar d\'aquí a :seconds segons.',
|
||||
|
||||
// Login & Register
|
||||
'sign_up' => 'Registra-m\'hi',
|
||||
@@ -28,7 +28,7 @@ return [
|
||||
'create_account' => 'Crea el compte',
|
||||
'already_have_account' => 'Ja teniu un compte?',
|
||||
'dont_have_account' => 'No teniu cap compte?',
|
||||
'social_login' => 'Inici de sessió social',
|
||||
'social_login' => 'Inici de sessió amb xarxes social',
|
||||
'social_registration' => 'Registre social',
|
||||
'social_registration_text' => 'Registreu-vos i inicieu la sessió fent servir un altre servei.',
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
// Buttons
|
||||
'cancel' => 'Cancel·la',
|
||||
'confirm' => 'D\'acord',
|
||||
'back' => 'Endarrere',
|
||||
'back' => 'Enrere',
|
||||
'save' => 'Desa',
|
||||
'continue' => 'Continua',
|
||||
'select' => 'Selecciona',
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Elimina',
|
||||
'add' => 'Afegeix',
|
||||
'fullscreen' => 'Pantalla completa',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Opcions d\'ordenació',
|
||||
@@ -47,7 +51,7 @@ return [
|
||||
'sort_ascending' => 'Ordre ascendent',
|
||||
'sort_descending' => 'Ordre descendent',
|
||||
'sort_name' => 'Nom',
|
||||
'sort_default' => 'Default',
|
||||
'sort_default' => 'Per defecte',
|
||||
'sort_created_at' => 'Data de creació',
|
||||
'sort_updated_at' => 'Data d\'actualització',
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Imatges',
|
||||
'my_recent_drafts' => 'Els vostres esborranys recents',
|
||||
'my_recently_viewed' => 'Les vostres visualitzacions recents',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'No heu vist cap pàgina',
|
||||
'no_pages_recently_created' => 'No s\'ha creat cap pàgina fa poc',
|
||||
'no_pages_recently_updated' => 'No s\'ha actualitzat cap pàgina fa poc',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'No s\'ha trobat la pàgina',
|
||||
'sorry_page_not_found' => 'No hem pogut trobar la pàgina que cerqueu.',
|
||||
'sorry_page_not_found_permission_warning' => 'Si esperàveu que existís, és possible que no tingueu permisos per a veure-la.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'Torna a l\'inici',
|
||||
'error_occurred' => 'S\'ha produït un error',
|
||||
'app_down' => ':appName està fora de servei en aquests moments',
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
return [
|
||||
|
||||
'password' => 'Les contrasenyes han de tenir com a mínim vuit caràcters i la confirmació ha de coincidir.',
|
||||
'user' => "No s'ha trobat cap usuari amb aquesta adreça electrònica.",
|
||||
'token' => 'El testimoni de reinicialització de la contrasenya no és vàlid per a aquesta adreça electrònica.',
|
||||
'sent' => 'Us hem enviat un enllaç per a restablir la contrasenya!',
|
||||
'reset' => 'S\'ha restablert la vostra contrasenya!',
|
||||
'user' => "No s'ha trobat cap usuari amb aquest correu electrònic.",
|
||||
'token' => 'El token de restabliment de contrasenya no és vàlid per aquest correu electrònic.',
|
||||
'sent' => 'T\'hem enviat un enllaç per a restablir la contrasenya!',
|
||||
'reset' => 'S\'ha restablert la teva contrasenya!',
|
||||
|
||||
];
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
return [
|
||||
|
||||
// Standard laravel validation lines
|
||||
'accepted' => 'Cal que accepteu el camp :attribute.',
|
||||
'active_url' => 'El camp :attribute no és un URL vàlid.',
|
||||
'accepted' => 'Cal que acceptis :attribute.',
|
||||
'active_url' => 'L\':attribute no és un URL vàlid.',
|
||||
'after' => 'El camp :attribute ha de ser una data posterior a :date.',
|
||||
'alpha' => 'El camp :attribute només pot contenir lletres.',
|
||||
'alpha_dash' => 'El camp :attribute només pot contenir lletres, números, guions i guions baixos.',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'odstranil/a knihovnu',
|
||||
'bookshelf_delete_notification' => 'Knihovna byla úspěšně odstraněna',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'okomentoval/a',
|
||||
'permissions_update' => 'updated permissions',
|
||||
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Odebrat',
|
||||
'add' => 'Přidat',
|
||||
'fullscreen' => 'Celá obrazovka',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Možnosti řazení',
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Obrázky',
|
||||
'my_recent_drafts' => 'Mé nedávné koncepty',
|
||||
'my_recently_viewed' => 'Mé nedávno zobrazené',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'Nezobrazili jste žádné stránky',
|
||||
'no_pages_recently_created' => 'Nedávno nebyly vytvořeny žádné stránky',
|
||||
'no_pages_recently_updated' => 'Nedávno nebyly aktualizovány žádné stránky',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'Stránka nenalezena',
|
||||
'sorry_page_not_found' => 'Omlouváme se, ale stránka, kterou hledáte nebyla nalezena.',
|
||||
'sorry_page_not_found_permission_warning' => 'Pokud očekáváte, že by stránka měla existovat, možná jen nemáte oprávnění pro její zobrazení.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'Návrat domů',
|
||||
'error_occurred' => 'Nastala chyba',
|
||||
'app_down' => ':appName je momentálně vypnutá',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'slettede bogreol',
|
||||
'bookshelf_delete_notification' => 'Bogreolen blev opdateret',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" has been added to your favourites',
|
||||
'favourite_remove_notification' => '":name" has been removed from your favourites',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'kommenterede til',
|
||||
'permissions_update' => 'Tilladelser opdateret',
|
||||
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Fjern',
|
||||
'add' => 'Tilføj',
|
||||
'fullscreen' => 'Fuld skærm',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sorteringsindstillinger',
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Billeder',
|
||||
'my_recent_drafts' => 'Mine seneste kladder',
|
||||
'my_recently_viewed' => 'Mine senest viste',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'My Favourites',
|
||||
'no_pages_viewed' => 'Du har ikke besøgt nogle sider',
|
||||
'no_pages_recently_created' => 'Ingen sider er blevet oprettet for nyligt',
|
||||
'no_pages_recently_updated' => 'Ingen sider er blevet opdateret for nyligt',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'Siden blev ikke fundet',
|
||||
'sorry_page_not_found' => 'Beklager, siden du leder efter blev ikke fundet.',
|
||||
'sorry_page_not_found_permission_warning' => 'Hvis du forventede, at denne side skulle eksistere, har du muligvis ikke tilladelse til at se den.',
|
||||
'image_not_found' => 'Image Not Found',
|
||||
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
|
||||
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
|
||||
'return_home' => 'Gå tilbage til hjem',
|
||||
'error_occurred' => 'Der opstod en fejl',
|
||||
'app_down' => ':appName er nede lige nu',
|
||||
|
||||
@@ -43,6 +43,10 @@ return [
|
||||
'bookshelf_delete' => 'hat das Bücherregal gelöscht',
|
||||
'bookshelf_delete_notification' => 'Das Bücherregal wurde erfolgreich gelöscht',
|
||||
|
||||
// Favourites
|
||||
'favourite_add_notification' => '":name" wurde zu deinen Favoriten hinzugefügt',
|
||||
'favourite_remove_notification' => '":name" wurde aus Ihren Favoriten entfernt',
|
||||
|
||||
// Other
|
||||
'commented_on' => 'hat einen Kommentar hinzugefügt',
|
||||
'permissions_update' => 'hat die Berechtigungen aktualisiert',
|
||||
|
||||
@@ -40,6 +40,10 @@ return [
|
||||
'remove' => 'Entfernen',
|
||||
'add' => 'Hinzufügen',
|
||||
'fullscreen' => 'Vollbild',
|
||||
'favourite' => 'Favorit',
|
||||
'unfavourite' => 'Kein Favorit',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sortieroptionen',
|
||||
@@ -47,7 +51,7 @@ return [
|
||||
'sort_ascending' => 'Aufsteigend sortieren',
|
||||
'sort_descending' => 'Absteigend sortieren',
|
||||
'sort_name' => 'Name',
|
||||
'sort_default' => 'Default',
|
||||
'sort_default' => 'Standard',
|
||||
'sort_created_at' => 'Erstellungsdatum',
|
||||
'sort_updated_at' => 'Aktualisierungsdatum',
|
||||
|
||||
@@ -65,7 +69,7 @@ return [
|
||||
'breadcrumb' => 'Brotkrumen',
|
||||
|
||||
// Header
|
||||
'header_menu_expand' => 'Expand Header Menu',
|
||||
'header_menu_expand' => 'Header-Menü erweitern',
|
||||
'profile_menu' => 'Profilmenü',
|
||||
'view_profile' => 'Profil ansehen',
|
||||
'edit_profile' => 'Profil bearbeiten',
|
||||
@@ -74,9 +78,9 @@ return [
|
||||
|
||||
// Layout tabs
|
||||
'tab_info' => 'Info',
|
||||
'tab_info_label' => 'Tab: Show Secondary Information',
|
||||
'tab_info_label' => 'Tab: Sekundäre Informationen anzeigen',
|
||||
'tab_content' => 'Inhalt',
|
||||
'tab_content_label' => 'Tab: Show Primary Content',
|
||||
'tab_content_label' => 'Tab: Hauptinhalt anzeigen',
|
||||
|
||||
// Email Content
|
||||
'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffnen Sie folgende URL in Ihrem Browser:',
|
||||
|
||||
@@ -27,6 +27,8 @@ return [
|
||||
'images' => 'Bilder',
|
||||
'my_recent_drafts' => 'Meine kürzlichen Entwürfe',
|
||||
'my_recently_viewed' => 'Kürzlich von mir angesehen',
|
||||
'my_most_viewed_favourites' => 'Meine meistgesehenen Favoriten',
|
||||
'my_favourites' => 'Meine Favoriten',
|
||||
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen',
|
||||
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt',
|
||||
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert',
|
||||
@@ -60,7 +62,7 @@ return [
|
||||
'search_permissions_set' => 'Berechtigungen gesetzt',
|
||||
'search_created_by_me' => 'Von mir erstellt',
|
||||
'search_updated_by_me' => 'Von mir aktualisiert',
|
||||
'search_owned_by_me' => 'Owned by me',
|
||||
'search_owned_by_me' => 'Besitzt von mir',
|
||||
'search_date_options' => 'Datums Optionen',
|
||||
'search_updated_before' => 'Aktualisiert vor',
|
||||
'search_updated_after' => 'Aktualisiert nach',
|
||||
|
||||
@@ -83,6 +83,9 @@ return [
|
||||
'404_page_not_found' => 'Seite nicht gefunden',
|
||||
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben, wurde nicht gefunden.',
|
||||
'sorry_page_not_found_permission_warning' => 'Wenn Sie erwartet haben, dass diese Seite existiert, haben Sie möglicherweise nicht die Berechtigung, sie anzuzeigen.',
|
||||
'image_not_found' => 'Bild nicht gefunden',
|
||||
'image_not_found_subtitle' => 'Entschuldigung. Das Bild, die Sie angefordert haben, wurde nicht gefunden.',
|
||||
'image_not_found_details' => 'Wenn Sie erwartet haben, dass dieses Bild existiert, könnte es gelöscht worden sein.',
|
||||
'return_home' => 'Zurück zur Startseite',
|
||||
'error_occurred' => 'Es ist ein Fehler aufgetreten',
|
||||
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user