mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-06 00:59:39 +03:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ef4cd80c3 | ||
|
|
e01f23583f | ||
|
|
b1ee1a856f | ||
|
|
4da72aa267 | ||
|
|
3dda622f0a | ||
|
|
7d951b842c | ||
|
|
94bf5b8fbb | ||
|
|
7792cb3915 | ||
|
|
be26253a18 | ||
|
|
3d5899d28c | ||
|
|
917d7428d6 | ||
|
|
bcc01bd8ff | ||
|
|
2c34a99248 | ||
|
|
789d17ab3f | ||
|
|
58117bcf2d | ||
|
|
b5caaa73b7 | ||
|
|
7997300f96 | ||
|
|
888f435651 | ||
|
|
1bdd1f8189 | ||
|
|
fa62c79b17 | ||
|
|
a8471b2c66 | ||
|
|
0627efe5e9 | ||
|
|
af7d62799c | ||
|
|
bb00c331e4 | ||
|
|
807f92b693 | ||
|
|
c5d31ea7b2 | ||
|
|
ef1bde8bb1 | ||
|
|
8897945609 | ||
|
|
9382d647d7 | ||
|
|
0d17d18d07 | ||
|
|
24eef03fb9 | ||
|
|
e51352e1a4 | ||
|
|
2dfb1ae3ee | ||
|
|
39928e1c63 | ||
|
|
40ca50e44f | ||
|
|
fc7b8c49fb | ||
|
|
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 | ||
|
|
f8b5a0fd50 | ||
|
|
99c42033b1 | ||
|
|
aad2ee675c | ||
|
|
a192b600fc | ||
|
|
b714652e10 |
@@ -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
|
||||
|
||||
|
||||
4
.github/translators.txt
vendored
4
.github/translators.txt
vendored
@@ -158,10 +158,12 @@ HenrijsS :: Latvian
|
||||
Pascal R-B (pborgner) :: German
|
||||
Boris (Ginfred) :: Russian
|
||||
Jonas Anker Rasmussen (jonasanker) :: Danish
|
||||
Gerwin de Keijzer (gdekeijzer) :: Dutch; German Informal; German
|
||||
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
|
||||
syn7ax69 :: Bulgarian; Turkish
|
||||
Blaade :: French
|
||||
|
||||
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,117 +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()
|
||||
->having('last_viewed_at', '>', 0)
|
||||
->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)
|
||||
|
||||
@@ -92,7 +92,7 @@ class Role extends Model implements Loggable
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* Get all visible roles.
|
||||
*/
|
||||
public static function visible(): Collection
|
||||
{
|
||||
@@ -104,7 +104,10 @@ class Role extends Model implements Loggable
|
||||
*/
|
||||
public static function restrictable(): Collection
|
||||
{
|
||||
return static::query()->where('system_name', '!=', 'admin')->get();
|
||||
return static::query()
|
||||
->where('system_name', '!=', 'admin')
|
||||
->orderBy('display_name', 'asc')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,7 +56,7 @@ return [
|
||||
'locale' => env('APP_LANG', 'en'),
|
||||
|
||||
// Locales available
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
|
||||
|
||||
// Application Fallback Locale
|
||||
'fallback_locale' => 'en',
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Facades\Theme;
|
||||
use BookStack\Theming\ThemeEvents;
|
||||
use BookStack\Util\HtmlContentFilter;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use DOMDocument;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
use Illuminate\Support\Str;
|
||||
use League\CommonMark\CommonMarkConverter;
|
||||
use League\CommonMark\Environment;
|
||||
use League\CommonMark\Extension\Table\TableExtension;
|
||||
@@ -31,6 +34,7 @@ class PageContent
|
||||
*/
|
||||
public function setNewHTML(string $html)
|
||||
{
|
||||
$html = $this->extractBase64Images($this->page, $html);
|
||||
$this->page->html = $this->formatHtml($html);
|
||||
$this->page->text = $this->toPlainText();
|
||||
$this->page->markdown = '';
|
||||
@@ -61,19 +65,65 @@ class PageContent
|
||||
return $converter->convertToHtml($markdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all base64 image data to saved images
|
||||
*/
|
||||
public function extractBase64Images(Page $page, string $htmlText): string
|
||||
{
|
||||
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
|
||||
return $htmlText;
|
||||
}
|
||||
|
||||
$doc = $this->loadDocumentFromHtml($htmlText);
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
$xPath = new DOMXPath($doc);
|
||||
$imageRepo = app()->make(ImageRepo::class);
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
// Get all img elements with image data blobs
|
||||
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
|
||||
foreach ($imageNodes as $imageNode) {
|
||||
$imageSrc = $imageNode->getAttribute('src');
|
||||
[$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
|
||||
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
|
||||
|
||||
// Validate extension
|
||||
if (!in_array($extension, $allowedExtensions)) {
|
||||
$imageNode->setAttribute('src', '');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save image from data with a random name
|
||||
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
|
||||
try {
|
||||
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id);
|
||||
$imageNode->setAttribute('src', $image->url);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$imageNode->setAttribute('src', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly within the system.
|
||||
*/
|
||||
protected function formatHtml(string $htmlText): string
|
||||
{
|
||||
if ($htmlText == '') {
|
||||
if (empty($htmlText)) {
|
||||
return $htmlText;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$doc = $this->loadDocumentFromHtml($htmlText);
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
@@ -112,7 +162,7 @@ class PageContent
|
||||
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
|
||||
{
|
||||
$old = str_replace('"', '', $old);
|
||||
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
|
||||
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
|
||||
foreach ($matchingLinks as $domElem) {
|
||||
$domElem->setAttribute('href', $new);
|
||||
}
|
||||
@@ -165,7 +215,7 @@ class PageContent
|
||||
/**
|
||||
* Render the page for viewing
|
||||
*/
|
||||
public function render(bool $blankIncludes = false) : string
|
||||
public function render(bool $blankIncludes = false): string
|
||||
{
|
||||
$content = $this->page->html;
|
||||
|
||||
@@ -191,9 +241,7 @@ class PageContent
|
||||
return [];
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc = $this->loadDocumentFromHtml($htmlContent);
|
||||
$xPath = new DOMXPath($doc);
|
||||
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||
|
||||
@@ -233,7 +281,7 @@ class PageContent
|
||||
/**
|
||||
* Remove any page include tags within the given HTML.
|
||||
*/
|
||||
protected function blankPageIncludes(string $html) : string
|
||||
protected function blankPageIncludes(string $html): string
|
||||
{
|
||||
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
|
||||
}
|
||||
@@ -241,7 +289,7 @@ class PageContent
|
||||
/**
|
||||
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
|
||||
*/
|
||||
protected function parsePageIncludes(string $html) : string
|
||||
protected function parsePageIncludes(string $html): string
|
||||
{
|
||||
$matches = [];
|
||||
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
|
||||
@@ -284,9 +332,7 @@ class PageContent
|
||||
protected function fetchSectionOfPage(Page $page, string $sectionId): string
|
||||
{
|
||||
$topLevelTags = ['table', 'ul', 'ol'];
|
||||
$doc = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$doc = $this->loadDocumentFromHtml($page->html);
|
||||
|
||||
// Search included content for the id given and blank out if not exists.
|
||||
$matchingElem = $doc->getElementById($sectionId);
|
||||
@@ -309,4 +355,16 @@ class PageContent
|
||||
|
||||
return $innerContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and load a DOMDocument from the given html content.
|
||||
*/
|
||||
protected function loadDocumentFromHtml(string $html): DOMDocument
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$html = '<body>' . $html . '</body>';
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
return $doc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,8 @@ class PageApiController extends ApiController
|
||||
*
|
||||
* Any HTML content provided should be kept to a single-block depth of plain HTML
|
||||
* elements to remain compatible with the BookStack front-end and editors.
|
||||
* Any images included via base64 data URIs will be extracted and saved as gallery
|
||||
* images against the page during upload.
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
|
||||
@@ -14,16 +14,14 @@ use Illuminate\Validation\ValidationException;
|
||||
class AttachmentController extends Controller
|
||||
{
|
||||
protected $attachmentService;
|
||||
protected $attachment;
|
||||
protected $pageRepo;
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
*/
|
||||
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
|
||||
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
|
||||
{
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->attachment = $attachment;
|
||||
$this->pageRepo = $pageRepo;
|
||||
}
|
||||
|
||||
@@ -67,7 +65,7 @@ class AttachmentController extends Controller
|
||||
'file' => 'required|file'
|
||||
]);
|
||||
|
||||
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('view', $attachment->page);
|
||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
@@ -89,7 +87,7 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function getUpdateForm(string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
|
||||
$this->checkOwnablePermission('page-update', $attachment->page);
|
||||
$this->checkOwnablePermission('attachment-create', $attachment);
|
||||
@@ -104,8 +102,8 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function update(Request $request, string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
|
||||
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
try {
|
||||
$this->validate($request, [
|
||||
'attachment_edit_name' => 'required|string|min:1|max:255',
|
||||
@@ -160,7 +158,7 @@ class AttachmentController extends Controller
|
||||
|
||||
$attachmentName = $request->get('attachment_link_name');
|
||||
$link = $request->get('attachment_link_url');
|
||||
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
|
||||
|
||||
return view('attachments.manager-link-form', [
|
||||
'pageId' => $pageId,
|
||||
@@ -202,9 +200,10 @@ class AttachmentController extends Controller
|
||||
* @throws FileNotFoundException
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function get(string $attachmentId)
|
||||
public function get(Request $request, string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
try {
|
||||
$page = $this->pageRepo->getById($attachment->uploaded_to);
|
||||
} catch (NotFoundException $exception) {
|
||||
@@ -217,8 +216,13 @@ class AttachmentController extends Controller
|
||||
return redirect($attachment->path);
|
||||
}
|
||||
|
||||
$fileName = $attachment->getFileName();
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
|
||||
|
||||
if ($request->get('open') === 'true') {
|
||||
return $this->inlineDownloadResponse($attachmentContents, $fileName);
|
||||
}
|
||||
return $this->downloadResponse($attachmentContents, $fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +231,8 @@ class AttachmentController extends Controller
|
||||
*/
|
||||
public function delete(string $attachmentId)
|
||||
{
|
||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||
/** @var Attachment $attachment */
|
||||
$attachment = Attachment::query()->findOrFail($attachmentId);
|
||||
$this->checkOwnablePermission('attachment-delete', $attachment);
|
||||
$this->attachmentService->deleteFile($attachment);
|
||||
return response()->json(['message' => trans('entities.attachments_deleted')]);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use BookStack\Facades\Activity;
|
||||
use BookStack\Interfaces\Loggable;
|
||||
use BookStack\HasCreatorAndUpdater;
|
||||
use BookStack\Model;
|
||||
use finfo;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
@@ -121,6 +122,20 @@ abstract class Controller extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file download response that provides the file with a content-type
|
||||
* correct for the file, in a way so the browser can show the content in browser.
|
||||
*/
|
||||
protected function inlineDownloadResponse(string $content, string $fileName): Response
|
||||
{
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => $mime,
|
||||
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a positive, successful notification to the user on next view load.
|
||||
*/
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?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;
|
||||
@@ -12,7 +14,6 @@ use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Throwable;
|
||||
use Views;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
@@ -141,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,
|
||||
@@ -149,7 +152,9 @@ class PageController extends Controller
|
||||
'current' => $page,
|
||||
'sidebarTree' => $sidebarTree,
|
||||
'commentsEnabled' => $commentsEnabled,
|
||||
'pageNav' => $pageNav
|
||||
'pageNav' => $pageNav,
|
||||
'next' => $nextPreviousLocator->getNext(),
|
||||
'previous' => $nextPreviousLocator->getPrevious(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -242,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
|
||||
]);
|
||||
}
|
||||
@@ -266,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,
|
||||
@@ -282,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,
|
||||
@@ -337,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
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -377,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();
|
||||
}
|
||||
|
||||
@@ -421,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();
|
||||
}
|
||||
|
||||
@@ -442,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]);
|
||||
|
||||
@@ -28,6 +28,7 @@ class Localization
|
||||
'es_AR' => 'es_AR',
|
||||
'fr' => 'fr_FR',
|
||||
'he' => 'he_IL',
|
||||
'hr' => 'hr_HR',
|
||||
'id' => 'id_ID',
|
||||
'it' => 'it_IT',
|
||||
'ja' => 'ja',
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ class ThemeService
|
||||
public function readThemeActions()
|
||||
{
|
||||
$themeActionsFile = theme_path('functions.php');
|
||||
if (file_exists($themeActionsFile)) {
|
||||
if ($themeActionsFile && file_exists($themeActionsFile)) {
|
||||
require $themeActionsFile;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ class FileLoader extends BaseLoader
|
||||
}
|
||||
|
||||
if (is_null($namespace) || $namespace === '*') {
|
||||
$themeTranslations = $this->loadPath(theme_path('lang'), $locale, $group);
|
||||
$themePath = theme_path('lang');
|
||||
$themeTranslations = $themePath ? $this->loadPath($themePath, $locale, $group) : [];
|
||||
$originalTranslations = $this->loadPath($this->path, $locale, $group);
|
||||
return array_merge($originalTranslations, $themeTranslations);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @property int id
|
||||
* @property string name
|
||||
* @property string path
|
||||
* @property string extension
|
||||
* @property ?Page page
|
||||
* @property bool external
|
||||
*/
|
||||
class Attachment extends Model
|
||||
@@ -31,9 +33,8 @@ class Attachment extends Model
|
||||
|
||||
/**
|
||||
* Get the page this file was uploaded to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
public function page(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Page::class, 'uploaded_to');
|
||||
}
|
||||
@@ -41,12 +42,12 @@ class Attachment extends Model
|
||||
/**
|
||||
* Get the url of this file.
|
||||
*/
|
||||
public function getUrl(): string
|
||||
public function getUrl($openInline = false): string
|
||||
{
|
||||
if ($this->external && strpos($this->path, 'http') !== 0) {
|
||||
return $this->path;
|
||||
}
|
||||
return url('/attachments/' . $this->id);
|
||||
return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
use Illuminate\Support\Str;
|
||||
use Log;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class AttachmentService
|
||||
@@ -38,11 +40,9 @@ class AttachmentService
|
||||
|
||||
/**
|
||||
* Get an attachment from storage.
|
||||
* @param Attachment $attachment
|
||||
* @return string
|
||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function getAttachmentFromStorage(Attachment $attachment)
|
||||
public function getAttachmentFromStorage(Attachment $attachment): string
|
||||
{
|
||||
return $this->getStorage()->get($attachment->path);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ class AttachmentService
|
||||
try {
|
||||
$storage->put($attachmentPath, $attachmentData);
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Error when attempting file upload:' . $e->getMessage());
|
||||
Log::error('Error when attempting file upload:' . $e->getMessage());
|
||||
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,17 @@
|
||||
use BookStack\Entities\Models\Page;
|
||||
use BookStack\Model;
|
||||
use BookStack\Traits\HasCreatorAndUpdater;
|
||||
use Images;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $url
|
||||
* @property string $path
|
||||
* @property string $type
|
||||
* @property int $uploaded_to
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
*/
|
||||
class Image extends Model
|
||||
{
|
||||
use HasCreatorAndUpdater;
|
||||
@@ -14,23 +23,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();
|
||||
}
|
||||
|
||||
@@ -130,6 +130,17 @@ class ImageRepo
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new image from an existing image data string.
|
||||
* @throws ImageUploadException
|
||||
*/
|
||||
public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0)
|
||||
{
|
||||
$image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
|
||||
$this->loadThumbs($image);
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a drawing the the database.
|
||||
* @throws ImageUploadException
|
||||
|
||||
@@ -140,12 +140,13 @@ class ImageService
|
||||
{
|
||||
$storage->put($path, $data);
|
||||
|
||||
// Set visibility if using s3 without an endpoint set.
|
||||
// Done since this call can break s3-like services but desired for actual
|
||||
// AWS s3 usage. Attempting to set ACL during above put request requires
|
||||
// different permissions hence would technically be a breaking change.
|
||||
// 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';
|
||||
if ($usingS3 && is_null(config('filesystems.disks.s3.endpoint'))) {
|
||||
$usingS3Like = $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
|
||||
if (!$usingS3Like) {
|
||||
$storage->setVisibility($path, 'public');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php namespace BookStack\Util;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use DOMNodeList;
|
||||
use DOMXPath;
|
||||
|
||||
@@ -16,6 +15,7 @@ class HtmlContentFilter
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html = '<body>' . $html . '</body>';
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
@@ -61,11 +61,10 @@ class HtmlContentFilter
|
||||
/**
|
||||
* Removed all of the given DOMNodes.
|
||||
*/
|
||||
static protected function removeNodes(DOMNodeList $nodes): void
|
||||
protected static function removeNodes(DOMNodeList $nodes): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +94,15 @@ function setting(string $key = null, $default = null)
|
||||
|
||||
/**
|
||||
* Get a path to a theme resource.
|
||||
* Returns null if a theme is not configured and
|
||||
* therefore a full path is not available for use.
|
||||
*/
|
||||
function theme_path(string $path = ''): string
|
||||
function theme_path(string $path = ''): ?string
|
||||
{
|
||||
$theme = config('view.theme');
|
||||
|
||||
if (!$theme) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
return base_path('themes/' . $theme .($path ? DIRECTORY_SEPARATOR.$path : $path));
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"php": "^7.3|^8.0",
|
||||
"ext-curl": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
|
||||
565
composer.lock
generated
565
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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']);
|
||||
});
|
||||
```
|
||||
151
package-lock.json
generated
151
package-lock.json
generated
@@ -6,19 +6,19 @@
|
||||
"": {
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^2.1.0",
|
||||
"esbuild": "0.7.8",
|
||||
"esbuild": "0.12.8",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.32.8"
|
||||
"sass": "^1.34.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
@@ -56,12 +56,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
@@ -184,9 +181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.60.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.60.0.tgz",
|
||||
"integrity": "sha512-AEL7LhFOlxPlCL8IdTcJDblJm8yrAGib7I+DErJPdZd4l6imx8IMgKK3RblVgBQqz3TZJR4oknQ03bz+uNjBYA=="
|
||||
"version": "5.61.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
|
||||
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -263,9 +260,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -313,10 +313,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
|
||||
"integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
|
||||
"version": "0.12.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz",
|
||||
"integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
}
|
||||
@@ -383,9 +384,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -674,12 +675,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
|
||||
"integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
|
||||
"version": "12.0.6",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
|
||||
"integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
@@ -979,12 +980,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.32.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.8.tgz",
|
||||
"integrity": "sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==",
|
||||
"version": "1.34.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz",
|
||||
"integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
@@ -1077,11 +1078,6 @@
|
||||
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
@@ -1227,12 +1223,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
@@ -1297,12 +1305,9 @@
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
@@ -1402,9 +1407,9 @@
|
||||
}
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.60.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.60.0.tgz",
|
||||
"integrity": "sha512-AEL7LhFOlxPlCL8IdTcJDblJm8yrAGib7I+DErJPdZd4l6imx8IMgKK3RblVgBQqz3TZJR4oknQ03bz+uNjBYA=="
|
||||
"version": "5.61.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.61.1.tgz",
|
||||
"integrity": "sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ=="
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "1.9.3",
|
||||
@@ -1472,9 +1477,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -1516,9 +1521,9 @@
|
||||
}
|
||||
},
|
||||
"esbuild": {
|
||||
"version": "0.7.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.7.8.tgz",
|
||||
"integrity": "sha512-6UT1nZB+8ja5avctUC6d3kGOUAhy6/ZYHljL4nk3++1ipadghBhUCAcwsTHsmUvdu04CcGKzo13mE+ZQ2O3zrA==",
|
||||
"version": "0.12.8",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.12.8.tgz",
|
||||
"integrity": "sha512-sx/LwlP/SWTGsd9G4RlOPrXnIihAJ2xwBUmzoqe2nWwbXORMQWtAGNJNYLBJJqa3e9PWvVzxdrtyFZJcr7D87g==",
|
||||
"dev": true
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
@@ -1565,9 +1570,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
@@ -1793,12 +1798,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.1.tgz",
|
||||
"integrity": "sha512-aU1TzmBKcWNNYvH9pjq6u92BML+Hz3h5S/QpfTFwiQF852pLT+9qHsrhM9JYipkOXZxGn+sGH8oyJE9FD9WezQ==",
|
||||
"version": "12.0.6",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.6.tgz",
|
||||
"integrity": "sha512-qv3sVLl4lMT96LLtR7xeRJX11OUFjsaD5oVat2/SNBIb21bJXwal2+SklcRbTwGwqWpWH/HRtYavOoJE+seL8w==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
@@ -2027,12 +2032,12 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.32.8",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.32.8.tgz",
|
||||
"integrity": "sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ==",
|
||||
"version": "1.34.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz",
|
||||
"integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -2110,11 +2115,6 @@
|
||||
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
@@ -2236,10 +2236,11 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.4.4",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"dev": true
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "4.0.1",
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^2.1.0",
|
||||
"esbuild": "0.7.8",
|
||||
"esbuild": "0.12.8",
|
||||
"livereload": "^0.9.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.32.8"
|
||||
"sass": "^1.34.1"
|
||||
},
|
||||
"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 |
47
resources/js/components/attachments-list.js
Normal file
47
resources/js/components/attachments-list.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Attachments List
|
||||
* Adds '?open=true' query to file attachment links
|
||||
* when ctrl/cmd is pressed down.
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AttachmentsList {
|
||||
|
||||
setup() {
|
||||
this.container = this.$el;
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
const isExpectedKey = (event) => event.key === 'Control' || event.key === 'Meta';
|
||||
window.addEventListener('keydown', event => {
|
||||
if (isExpectedKey(event)) {
|
||||
this.addOpenQueryToLinks();
|
||||
}
|
||||
}, {passive: true});
|
||||
window.addEventListener('keyup', event => {
|
||||
if (isExpectedKey(event)) {
|
||||
this.removeOpenQueryFromLinks();
|
||||
}
|
||||
}, {passive: true});
|
||||
}
|
||||
|
||||
addOpenQueryToLinks() {
|
||||
const links = this.container.querySelectorAll('a.attachment-file');
|
||||
for (const link of links) {
|
||||
if (link.href.split('?')[1] !== 'open=true') {
|
||||
link.href = link.href + '?open=true';
|
||||
link.setAttribute('target', '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeOpenQueryFromLinks() {
|
||||
const links = this.container.querySelectorAll('a.attachment-file');
|
||||
for (const link of links) {
|
||||
link.href = link.href.split('?')[0];
|
||||
link.removeAttribute('target');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AttachmentsList;
|
||||
@@ -2,6 +2,7 @@ import addRemoveRows from "./add-remove-rows.js"
|
||||
import ajaxDeleteRow from "./ajax-delete-row.js"
|
||||
import ajaxForm from "./ajax-form.js"
|
||||
import attachments from "./attachments.js"
|
||||
import attachmentsList from "./attachments-list.js"
|
||||
import autoSuggest from "./auto-suggest.js"
|
||||
import backToTop from "./back-to-top.js"
|
||||
import bookSort from "./book-sort.js"
|
||||
@@ -56,6 +57,7 @@ const componentMapping = {
|
||||
"ajax-delete-row": ajaxDeleteRow,
|
||||
"ajax-form": ajaxForm,
|
||||
"attachments": attachments,
|
||||
"attachments-list": attachmentsList,
|
||||
"auto-suggest": autoSuggest,
|
||||
"back-to-top": backToTop,
|
||||
"book-sort": bookSort,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' => 'تحديث الأذونات',
|
||||
|
||||
@@ -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' => 'لم تُحدّث أي صفحات مؤخراً',
|
||||
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -33,13 +33,17 @@ return [
|
||||
'copy' => 'Копирай',
|
||||
'reply' => 'Отговори',
|
||||
'delete' => 'Изтрий',
|
||||
'delete_confirm' => 'Confirm Deletion',
|
||||
'delete_confirm' => 'Потвърдете изтриването',
|
||||
'search' => 'Търси',
|
||||
'search_clear' => 'Изчисти търсенето',
|
||||
'reset' => 'Нулирай',
|
||||
'remove' => 'Премахване',
|
||||
'add' => 'Добави',
|
||||
'fullscreen' => 'Пълен екран',
|
||||
'favourite' => 'Favourite',
|
||||
'unfavourite' => 'Unfavourite',
|
||||
'next' => 'Next',
|
||||
'previous' => 'Previous',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Опции за сортиране',
|
||||
@@ -84,6 +88,6 @@ return [
|
||||
|
||||
// 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' => 'Are you sure you want to delete this image?',
|
||||
'image_delete_confirm_text' => 'Сигурни ли сте, че искате да изтриете това изображение?',
|
||||
'image_select_image' => 'Изберете изображение',
|
||||
'image_dropzone' => 'Поставете тук изображение или кликнете тук за да качите',
|
||||
'images_deleted' => 'Изображението е изтрито',
|
||||
|
||||
@@ -22,11 +22,13 @@ return [
|
||||
'meta_created_name' => 'Създадено преди :timeLength от :user',
|
||||
'meta_updated' => 'Актуализирано :timeLength',
|
||||
'meta_updated_name' => 'Актуализирано преди :timeLength от :user',
|
||||
'meta_owned_name' => 'Owned by :user',
|
||||
'meta_owned_name' => 'Притежавано от :user',
|
||||
'entity_select' => 'Избор на обект',
|
||||
'images' => 'Изображения',
|
||||
'my_recent_drafts' => 'Моите скорошни драфтове',
|
||||
'my_recently_viewed' => 'Моите скорошни преглеждания',
|
||||
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
|
||||
'my_favourites' => 'Моите фаворити',
|
||||
'no_pages_viewed' => 'Не сте прегледали никакви страници',
|
||||
'no_pages_recently_created' => 'Не са били създавани страници скоро',
|
||||
'no_pages_recently_updated' => 'Не са били актуализирани страници скоро',
|
||||
@@ -40,7 +42,7 @@ return [
|
||||
'permissions_intro' => 'Веднъж добавени, тези права ще вземат приоритет над всички други установени права.',
|
||||
'permissions_enable' => 'Разреши уникални права',
|
||||
'permissions_save' => 'Запази права',
|
||||
'permissions_owner' => 'Owner',
|
||||
'permissions_owner' => 'Собственик',
|
||||
|
||||
// Search
|
||||
'search_results' => 'Резултати от търсенето',
|
||||
@@ -49,7 +51,7 @@ return [
|
||||
'search_no_pages' => 'Няма страници отговарящи на търсенето',
|
||||
'search_for_term' => 'Търси :term',
|
||||
'search_more' => 'Още резултати',
|
||||
'search_advanced' => 'Advanced Search',
|
||||
'search_advanced' => 'Подробно търсене',
|
||||
'search_terms' => 'Search Terms',
|
||||
'search_content_type' => 'Тип на съдържание',
|
||||
'search_exact_matches' => 'Точни съвпадения',
|
||||
@@ -60,7 +62,7 @@ return [
|
||||
'search_permissions_set' => 'Задаване на права',
|
||||
'search_created_by_me' => 'Създадено от мен',
|
||||
'search_updated_by_me' => 'Обновено от мен',
|
||||
'search_owned_by_me' => 'Owned by me',
|
||||
'search_owned_by_me' => 'Притежаван от мен',
|
||||
'search_date_options' => 'Настройки на дати',
|
||||
'search_updated_before' => 'Обновено преди',
|
||||
'search_updated_after' => 'Обновено след',
|
||||
@@ -260,7 +262,7 @@ return [
|
||||
'attachments_upload' => 'Прикачен файл',
|
||||
'attachments_link' => 'Прикачване на линк',
|
||||
'attachments_set_link' => 'Поставяне на линк',
|
||||
'attachments_delete' => 'Are you sure you want to delete this attachment?',
|
||||
'attachments_delete' => 'Сигурни ли сте, че искате да изтриете прикачения файл?',
|
||||
'attachments_dropzone' => 'Поставете файлове или цъкнете тук за да прикачите файл',
|
||||
'attachments_no_files' => 'Няма прикачени фалове',
|
||||
'attachments_explain_link' => 'Може да прикачите линк, ако не искате да качвате файл. Този линк може да бъде към друга страница или към файл в облакова пространство.',
|
||||
|
||||
@@ -35,13 +35,13 @@ return [
|
||||
'app_primary_color' => 'Основен цвят на приложението',
|
||||
'app_primary_color_desc' => 'Изберете основния цвят на приложението, включително на банера, бутоните и линковете.',
|
||||
'app_homepage' => 'Application Homepage',
|
||||
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
|
||||
'app_homepage_select' => 'Select a page',
|
||||
'app_footer_links' => 'Footer Links',
|
||||
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
|
||||
'app_homepage_desc' => 'Изберете начална страница, която ще замени изгледа по подразбиране. Дефинираните права на страницата, която е избрана ще бъдат игнорирани.',
|
||||
'app_homepage_select' => 'Избери страница',
|
||||
'app_footer_links' => 'Футър линкове',
|
||||
'app_footer_links_desc' => 'Добави линк в съдържанието на футъра. Добавените линкове ще се показват долу в повечето страници, включително и в страниците, в които логването не е задължително. Можете да използвате заместител "trans::<key>", за да използвате дума дефинирана от системата. Пример: Използването на "trans::common.privacy_policy" ще покаже "Лични данни" или на "trans::common.terms_of_service" ще покаже "Общи условия".',
|
||||
'app_footer_links_label' => 'Link Label',
|
||||
'app_footer_links_url' => 'Link URL',
|
||||
'app_footer_links_add' => 'Add Footer Link',
|
||||
'app_footer_links_url' => 'Линк URL',
|
||||
'app_footer_links_add' => 'Добави футър линк',
|
||||
'app_disable_comments' => 'Disable Comments',
|
||||
'app_disable_comments_toggle' => 'Disable comments',
|
||||
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
|
||||
@@ -80,20 +80,20 @@ return [
|
||||
'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
|
||||
'maint_send_test_email' => 'Send a Test Email',
|
||||
'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
|
||||
'maint_send_test_email_run' => 'Send test email',
|
||||
'maint_send_test_email_success' => 'Email sent to :address',
|
||||
'maint_send_test_email_mail_subject' => 'Test Email',
|
||||
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
|
||||
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
|
||||
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
|
||||
'maint_recycle_bin_open' => 'Open Recycle Bin',
|
||||
'maint_send_test_email_run' => 'Изпрати тестов имейл',
|
||||
'maint_send_test_email_success' => 'Имейл изпратен на :address',
|
||||
'maint_send_test_email_mail_subject' => 'Тестов Имейл',
|
||||
'maint_send_test_email_mail_greeting' => 'Изпращането на Имейл работи!',
|
||||
'maint_send_test_email_mail_text' => 'Поздравления! След като получихте този имейл, Вашите имейл настройки са конфигурирани правилно.',
|
||||
'maint_recycle_bin_desc' => 'Изтрити рафти, книги, глави и страници се преместват в кошчето, откъдето можете да ги възстановите или изтриете завинаги. Стари съдържания в кошчето ще бъдат изтрити автоматично след време, в зависимост от настройките на системата.',
|
||||
'maint_recycle_bin_open' => 'Отвори Кошчето',
|
||||
|
||||
// Recycle Bin
|
||||
'recycle_bin' => 'Recycle Bin',
|
||||
'recycle_bin' => 'Кошче',
|
||||
'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'recycle_bin_deleted_item' => 'Deleted Item',
|
||||
'recycle_bin_deleted_by' => 'Deleted By',
|
||||
'recycle_bin_deleted_at' => 'Deletion Time',
|
||||
'recycle_bin_deleted_item' => 'Изтрит предмет',
|
||||
'recycle_bin_deleted_by' => 'Изтрит от',
|
||||
'recycle_bin_deleted_at' => 'Час на изтриване',
|
||||
'recycle_bin_permanently_delete' => 'Permanently Delete',
|
||||
'recycle_bin_restore' => 'Restore',
|
||||
'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
|
||||
@@ -111,27 +111,27 @@ return [
|
||||
'audit' => 'Audit Log',
|
||||
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
|
||||
'audit_event_filter' => 'Event Filter',
|
||||
'audit_event_filter_no_filter' => 'No Filter',
|
||||
'audit_deleted_item' => 'Deleted Item',
|
||||
'audit_deleted_item_name' => 'Name: :name',
|
||||
'audit_table_user' => 'User',
|
||||
'audit_table_event' => 'Event',
|
||||
'audit_event_filter_no_filter' => 'Без филтър',
|
||||
'audit_deleted_item' => 'Изтрит предмет',
|
||||
'audit_deleted_item_name' => 'Име: :name',
|
||||
'audit_table_user' => 'Потребител',
|
||||
'audit_table_event' => 'Събитие',
|
||||
'audit_table_related' => 'Related Item or Detail',
|
||||
'audit_table_date' => 'Activity Date',
|
||||
'audit_date_from' => 'Date Range From',
|
||||
'audit_date_to' => 'Date Range To',
|
||||
'audit_table_date' => 'Дата на активност',
|
||||
'audit_date_from' => 'Време от',
|
||||
'audit_date_to' => 'Време до',
|
||||
|
||||
// Role Settings
|
||||
'roles' => 'Roles',
|
||||
'role_user_roles' => 'User Roles',
|
||||
'role_create' => 'Create New Role',
|
||||
'role_create_success' => 'Role successfully created',
|
||||
'role_delete' => 'Delete Role',
|
||||
'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
|
||||
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
|
||||
'role_delete_no_migration' => "Don't migrate users",
|
||||
'role_delete_sure' => 'Are you sure you want to delete this role?',
|
||||
'role_delete_success' => 'Role successfully deleted',
|
||||
'roles' => 'Роли',
|
||||
'role_user_roles' => 'Потребителски роли',
|
||||
'role_create' => 'Създай нова роля',
|
||||
'role_create_success' => 'Ролята беше успешно създадена',
|
||||
'role_delete' => 'Изтрий роля',
|
||||
'role_delete_confirm' => 'Това ще изтрие ролята \':roleName\'.',
|
||||
'role_delete_users_assigned' => 'В тази роля се намират :userCount потребители. Ако искате да преместите тези потребители в друга роля, моля изберете нова роля отдолу.',
|
||||
'role_delete_no_migration' => "Не премествай потребителите в нова роля",
|
||||
'role_delete_sure' => 'Сигурни ли сте, че искате да изтриете тази роля?',
|
||||
'role_delete_success' => 'Ролята беше успешно изтрита',
|
||||
'role_edit' => 'Редактиране на роля',
|
||||
'role_details' => 'Детайли на роля',
|
||||
'role_name' => 'Име на ролята',
|
||||
@@ -146,24 +146,24 @@ return [
|
||||
'role_access_api' => 'Достъп до API на системата',
|
||||
'role_manage_settings' => 'Управление на настройките на приложението',
|
||||
'role_asset' => 'Настройки за достъп до активи',
|
||||
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
|
||||
'role_asset_desc' => 'Тези настойки за достъп контролират достъпа по подразбиране до активите в системата. Настойките за достъп до книги, глави и страници ще отменят тези настройки.',
|
||||
'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',
|
||||
'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',
|
||||
'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.',
|
||||
'role_all' => 'Всички',
|
||||
'role_own' => 'Собствени',
|
||||
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
|
||||
'role_save' => 'Save Role',
|
||||
'role_update_success' => 'Role successfully updated',
|
||||
'role_users' => 'Users in this role',
|
||||
'role_users_none' => 'No users are currently assigned to this role',
|
||||
'role_save' => 'Запази ролята',
|
||||
'role_update_success' => 'Ролята беше успешно актуализирана',
|
||||
'role_users' => 'Потребители в тази роля',
|
||||
'role_users_none' => 'В момента няма потребители, назначени за тази роля',
|
||||
|
||||
// Users
|
||||
'users' => 'Users',
|
||||
'user_profile' => 'User Profile',
|
||||
'users_add_new' => 'Add New User',
|
||||
'users_search' => 'Search Users',
|
||||
'users_latest_activity' => 'Latest Activity',
|
||||
'users_details' => 'User Details',
|
||||
'users' => 'Потребители',
|
||||
'user_profile' => 'Потребителски профил',
|
||||
'users_add_new' => 'Добави нов потребител',
|
||||
'users_search' => 'Търси Потребители',
|
||||
'users_latest_activity' => 'Последна активност',
|
||||
'users_details' => 'Потребителски детайли',
|
||||
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
|
||||
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
|
||||
'users_role' => 'User Roles',
|
||||
@@ -176,13 +176,13 @@ return [
|
||||
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
|
||||
'users_password_warning' => 'Only fill the below if you would like to change your password.',
|
||||
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
|
||||
'users_delete' => 'Delete User',
|
||||
'users_delete_named' => 'Delete user :userName',
|
||||
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
|
||||
'users_delete_confirm' => 'Are you sure you want to delete this user?',
|
||||
'users_migrate_ownership' => 'Migrate Ownership',
|
||||
'users_delete' => 'Изтрий потребител',
|
||||
'users_delete_named' => 'Изтрий потребителя :userName',
|
||||
'users_delete_warning' => 'Това изцяло ще изтрие този потребител с името \':userName\' от системата.',
|
||||
'users_delete_confirm' => 'Сигурни ли сте, че искате да изтриете този потребител?',
|
||||
'users_migrate_ownership' => 'Мигрирайте собствеността на сайта',
|
||||
'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
|
||||
'users_none_selected' => 'No user selected',
|
||||
'users_none_selected' => 'Няма избрани потребители',
|
||||
'users_delete_success' => 'User successfully removed',
|
||||
'users_edit' => 'Edit User',
|
||||
'users_edit_profile' => 'Edit Profile',
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -8,67 +8,67 @@
|
||||
return [
|
||||
|
||||
// Standard laravel validation lines
|
||||
'accepted' => 'The :attribute must be accepted.',
|
||||
'active_url' => 'The :attribute is not a valid URL.',
|
||||
'after' => 'The :attribute must be a date after :date.',
|
||||
'alpha' => 'The :attribute may only contain letters.',
|
||||
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
|
||||
'alpha_num' => 'The :attribute may only contain letters and numbers.',
|
||||
'array' => 'The :attribute must be an array.',
|
||||
'before' => 'The :attribute must be a date before :date.',
|
||||
'accepted' => ':attribute трябва да бъде одобрен.',
|
||||
'active_url' => ':attribute не е валиден URL адрес.',
|
||||
'after' => ':attribute трябва да е дата след :date.',
|
||||
'alpha' => ':attribute може да съдържа само букви.',
|
||||
'alpha_dash' => ':attribute може да съдържа само букви, числа, тире и долна черта.',
|
||||
'alpha_num' => ':attribute може да съдържа само букви и числа.',
|
||||
'array' => ':attribute трябва да е масив (array).',
|
||||
'before' => ':attribute трябва да е дата след :date.',
|
||||
'between' => [
|
||||
'numeric' => 'The :attribute must be between :min and :max.',
|
||||
'file' => 'The :attribute must be between :min and :max kilobytes.',
|
||||
'string' => 'The :attribute must be between :min and :max characters.',
|
||||
'numeric' => ':attribute трябва да е между :min и :max.',
|
||||
'file' => ':attribute трябва да е между :min и :max килобайта.',
|
||||
'string' => 'Дължината на :attribute трябва да бъде между :min и :max символа.',
|
||||
'array' => 'The :attribute must have between :min and :max items.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'confirmed' => 'The :attribute confirmation does not match.',
|
||||
'date' => 'The :attribute is not a valid date.',
|
||||
'date_format' => 'The :attribute does not match the format :format.',
|
||||
'different' => 'The :attribute and :other must be different.',
|
||||
'digits' => 'The :attribute must be :digits digits.',
|
||||
'digits_between' => 'The :attribute must be between :min and :max digits.',
|
||||
'email' => 'The :attribute must be a valid email address.',
|
||||
'ends_with' => 'The :attribute must end with one of the following: :values',
|
||||
'filled' => 'The :attribute field is required.',
|
||||
'boolean' => 'Полето :attribute трябва да съдържа булева стойност (true или false).',
|
||||
'confirmed' => 'Потвърждението на :attribute не съвпада.',
|
||||
'date' => ':attribute не е валидна дата.',
|
||||
'date_format' => ':attribute не е в посоченият формат - :format.',
|
||||
'different' => ':attribute и :other трябва да са различни.',
|
||||
'digits' => ':attribute трябва да съдържа :digits цифри.',
|
||||
'digits_between' => ':attribute трябва да бъде с дължина между :min и :max цифри.',
|
||||
'email' => ':attribute трябва да бъде валиден имейл адрес.',
|
||||
'ends_with' => ':attribute трябва да свършва с един от следните символи: :values',
|
||||
'filled' => 'Полето :attribute е задължителен.',
|
||||
'gt' => [
|
||||
'numeric' => 'The :attribute must be greater than :value.',
|
||||
'file' => 'The :attribute must be greater than :value kilobytes.',
|
||||
'string' => 'The :attribute must be greater than :value characters.',
|
||||
'numeric' => ':attribute трябва да бъде по-голям от :value.',
|
||||
'file' => 'Големината на :attribute трябва да бъде по-голямо от :value килобайта.',
|
||||
'string' => 'Дължината на :attribute трябва да бъде по-голямо от :value символа.',
|
||||
'array' => 'The :attribute must have more than :value items.',
|
||||
],
|
||||
'gte' => [
|
||||
'numeric' => 'The :attribute must be greater than or equal :value.',
|
||||
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
|
||||
'string' => 'The :attribute must be greater than or equal :value characters.',
|
||||
'file' => 'Големината на :attribute трябва да бъде по-голямо или равно на :value килобайта.',
|
||||
'string' => 'Дължината на :attribute трябва да бъде по-голямо или равно на :value символа.',
|
||||
'array' => 'The :attribute must have :value items or more.',
|
||||
],
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'image' => 'The :attribute must be an image.',
|
||||
'image_extension' => 'The :attribute must have a valid & supported image extension.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'integer' => 'The :attribute must be an integer.',
|
||||
'ip' => 'The :attribute must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute must be a valid JSON string.',
|
||||
'exists' => 'Избраният :attribute е невалиден.',
|
||||
'image' => ':attribute трябва да e изображение.',
|
||||
'image_extension' => ':attribute трябва да е валиден и/или допустим графичен файлов формат.',
|
||||
'in' => 'Избраният :attribute е невалиден.',
|
||||
'integer' => ':attribute трябва да бъде цяло число.',
|
||||
'ip' => ':attribute трябва да бъде валиден IP адрес.',
|
||||
'ipv4' => ':attribute трябва да бъде валиден IPv4 адрес.',
|
||||
'ipv6' => ':attribute трябва да бъде валиден IPv6 адрес.',
|
||||
'json' => ':attribute трябва да съдържа валиден JSON.',
|
||||
'lt' => [
|
||||
'numeric' => 'The :attribute must be less than :value.',
|
||||
'file' => 'The :attribute must be less than :value kilobytes.',
|
||||
'string' => 'The :attribute must be less than :value characters.',
|
||||
'numeric' => ':attribute трябва да бъде по-малко от :value.',
|
||||
'file' => 'Големината на :attribute трябва да бъде по-малко от :value килобайта.',
|
||||
'string' => 'Дължината на :attribute трябва да бъде по-малко от :value символа.',
|
||||
'array' => 'The :attribute must have less than :value items.',
|
||||
],
|
||||
'lte' => [
|
||||
'numeric' => 'The :attribute must be less than or equal :value.',
|
||||
'file' => 'The :attribute must be less than or equal :value kilobytes.',
|
||||
'string' => 'The :attribute must be less than or equal :value characters.',
|
||||
'numeric' => ':attribute трябва да бъде по-малко или равно на :value.',
|
||||
'file' => 'Големината на :attribute трябва да бъде по-малко или равно на :value килобайта.',
|
||||
'string' => 'Дължината на :attribute трябва да бъде по-малко или равно на :value символа.',
|
||||
'array' => 'The :attribute must not have more than :value items.',
|
||||
],
|
||||
'max' => [
|
||||
'numeric' => 'The :attribute may not be greater than :max.',
|
||||
'file' => 'The :attribute may not be greater than :max kilobytes.',
|
||||
'string' => 'The :attribute may not be greater than :max characters.',
|
||||
'numeric' => ':attribute не трябва да бъде по-голям от :max.',
|
||||
'file' => 'Големината на :attribute не може да бъде по-голямо от :value килобайта.',
|
||||
'string' => 'Дължината на :attribute не може да бъде по-голямо от :value символа.',
|
||||
'array' => 'The :attribute may not have more than :max items.',
|
||||
],
|
||||
'mimes' => 'The :attribute must be a file of type: :values.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ó',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'עברית',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -241,6 +241,7 @@ return [
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'Hebraisk',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
@@ -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' => 'Nächste',
|
||||
'previous' => 'Vorheriges',
|
||||
|
||||
// Sort Options
|
||||
'sort_options' => 'Sortieroptionen',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -83,9 +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' => '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.',
|
||||
'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.',
|
||||
|
||||
@@ -244,6 +244,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung
|
||||
'es_AR' => 'Español Argentina',
|
||||
'fr' => 'Français',
|
||||
'he' => 'Hebräisch',
|
||||
'hr' => 'Hrvatski',
|
||||
'hu' => 'Magyar',
|
||||
'id' => 'Bahasa Indonesia',
|
||||
'it' => 'Italian',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user