*/
public function getUrl()
{
+ if ($this->external && strpos($this->path, 'http') !== 0) {
+ return $this->path;
+ }
return baseUrl('/attachments/' . $this->id);
}
}
protected $fillable = ['name', 'description', 'priority', 'book_id'];
- protected $with = ['book'];
-
/**
* Get the book this chapter is within.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
* @param $path
* @return string
*/
- public function getUrl($path)
+ public function getUrl($path = '/')
{
- return '/';
+ return $path;
}
}
use Exception;
use Illuminate\Auth\AuthenticationException;
-use Illuminate\Http\Request;
-use Illuminate\Pipeline\Pipeline;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
*
* @param \Exception $e
* @return mixed
+ * @throws Exception
*/
public function report(Exception $e)
{
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
- return $this->loadErrorMiddleware($request, function ($request) use ($e) {
- $message = $e->getMessage() ?: trans('errors.404_page_not_found');
- return response()->view('errors/404', ['message' => $message], 404);
- });
+ return \Route::respondWithRoute('fallback');
}
return parent::render($request, $e);
}
- /**
- * Load the middleware required to show state/session-enabled error pages.
- * @param Request $request
- * @param $callback
- * @return mixed
- */
- protected function loadErrorMiddleware(Request $request, $callback)
- {
- $middleware = (\Route::getMiddlewareGroups()['web_errors']);
- return (new Pipeline($this->container))
- ->send($request)
- ->through($middleware)
- ->then($callback);
- }
-
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
- 'link' => 'url|min:1|max:255'
+ 'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
- 'link' => 'required|url|min:1|max:255'
+ 'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
* @param $attachmentId
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ * @throws NotFoundException
*/
public function get($attachmentId)
{
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
- // Custom homepage
+
$customHomepage = false;
- $homepageSetting = setting('app-homepage');
- if ($homepageSetting) {
- $id = intval(explode(':', $homepageSetting)[0]);
- $customHomepage = $this->entityRepo->getById('page', $id, false, true);
- $this->entityRepo->renderPage($customHomepage, true);
+ $books = false;
+ $booksViewType = false;
+
+ // Check book homepage
+ $bookHomepageSetting = setting('app-book-homepage');
+ if ($bookHomepageSetting) {
+ $books = $this->entityRepo->getAllPaginated('book', 18);
+ $booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
+ } else {
+ // Check custom homepage
+ $homepageSetting = setting('app-homepage');
+ if ($homepageSetting) {
+ $id = intval(explode(':', $homepageSetting)[0]);
+ $customHomepage = $this->entityRepo->getById('page', $id, false, true);
+ $this->entityRepo->renderPage($customHomepage, true);
+ }
+ }
+
+ $view = 'home';
+ if ($bookHomepageSetting) {
+ $view = 'home-book';
+ } else if ($customHomepage) {
+ $view = 'home-custom';
}
- $view = $customHomepage ? 'home-custom' : 'home';
- return view($view, [
+ return view('common/' . $view, [
'activity' => $activity,
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
- 'customHomepage' => $customHomepage
+ 'customHomepage' => $customHomepage,
+ 'books' => $books,
+ 'booksViewType' => $booksViewType
]);
}
]);
}
- /**
- * Get an icon via image request.
- * Can provide a 'color' parameter with hex value to color the icon.
- * @param $iconName
- * @param Request $request
- * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
- */
- public function getIcon($iconName, Request $request)
- {
- $attrs = [];
- if ($request->filled('color')) {
- $attrs['fill'] = '#' . $request->get('color');
- }
-
- $icon = icon($iconName, $attrs);
- return response($icon, 200, [
- 'Content-Type' => 'image/svg+xml',
- 'Cache-Control' => 'max-age=3600',
- ]);
- }
-
/**
* Get custom head HTML, Used in ajax calls to show in editor.
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
$allowRobots = $sitePublic;
}
return response()
- ->view('robots', ['allowRobots' => $allowRobots])
+ ->view('common/robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
+
+ /**
+ * Show the route for 404 responses.
+ */
+ public function getNotFound()
+ {
+ return response()->view('errors/404', [], 404);
+ }
}
return redirect()->back();
}
+ $this->checkOwnablePermission('page-create', $parent);
+
$this->entityRepo->changePageParent($page, $parent);
Activity::add($page, 'page_move', $page->book->id);
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
return redirect($page->getUrl());
}
+ /**
+ * Show the view to copy a page.
+ * @param string $bookSlug
+ * @param string $pageSlug
+ * @return mixed
+ * @throws NotFoundException
+ */
+ public function showCopy($bookSlug, $pageSlug)
+ {
+ $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+ $this->checkOwnablePermission('page-update', $page);
+ session()->flashInput(['name' => $page->name]);
+ return view('pages/copy', [
+ 'book' => $page->book,
+ 'page' => $page
+ ]);
+ }
+
+ /**
+ * Create a copy of a page within the requested target destination.
+ * @param string $bookSlug
+ * @param string $pageSlug
+ * @param Request $request
+ * @return mixed
+ * @throws NotFoundException
+ */
+ public function copy($bookSlug, $pageSlug, Request $request)
+ {
+ $page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
+ $this->checkOwnablePermission('page-update', $page);
+
+ $entitySelection = $request->get('entity_selection', null);
+ if ($entitySelection === null || $entitySelection === '') {
+ $parent = $page->chapter ? $page->chapter : $page->book;
+ } else {
+ $stringExploded = explode(':', $entitySelection);
+ $entityType = $stringExploded[0];
+ $entityId = intval($stringExploded[1]);
+
+ try {
+ $parent = $this->entityRepo->getById($entityType, $entityId);
+ } catch (\Exception $e) {
+ session()->flash(trans('entities.selected_book_chapter_not_found'));
+ return redirect()->back();
+ }
+ }
+
+ $this->checkOwnablePermission('page-create', $parent);
+
+ $pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
+
+ Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
+ session()->flash('success', trans('entities.pages_copy_success'));
+
+ return redirect($pageCopy->getUrl());
+ }
+
/**
* Set the permissions for this page.
* @param string $bookSlug
* @param string $pageSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
+ * @throws NotFoundException
*/
public function restrict($bookSlug, $pageSlug, Request $request)
{
{
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = $request->get('term', false);
+ $permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
- $entities = $this->searchService->searchEntities($searchTerm)['results'];
+ $entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);
})->toArray();
- $entities = $this->viewService->getPopular(20, 0, $entityNames);
+ $entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
- 'web_errors' => [
- \BookStack\Http\Middleware\EncryptCookies::class,
- \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
- \Illuminate\Session\Middleware\StartSession::class,
- \Illuminate\View\Middleware\ShareErrorsFromSession::class,
- \BookStack\Http\Middleware\VerifyCsrfToken::class,
- \BookStack\Http\Middleware\Localization::class
- ],
'api' => [
'throttle:60,1',
'bindings',
protected $simpleAttributes = ['name', 'id', 'slug'];
- protected $with = ['book'];
public $textField = 'text';
/**
return $slug;
}
+ /**
+ * Get a new draft page instance.
+ * @param Book $book
+ * @param Chapter|bool $chapter
+ * @return Page
+ */
+ public function getDraftPage(Book $book, $chapter = false)
+ {
+ $page = $this->page->newInstance();
+ $page->name = trans('entities.pages_initial_name');
+ $page->created_by = user()->id;
+ $page->updated_by = user()->id;
+ $page->draft = true;
+
+ if ($chapter) {
+ $page->chapter_id = $chapter->id;
+ }
+
+ $book->pages()->save($page);
+ $page = $this->page->find($page->id);
+ $this->permissionService->buildJointPermissionsForEntity($page);
+ return $page;
+ }
+
/**
* Publish a draft page to make it a normal page.
* Sets the slug and updates the content.
return $draftPage;
}
+ /**
+ * Create a copy of a page in a new location with a new name.
+ * @param Page $page
+ * @param Entity $newParent
+ * @param string $newName
+ * @return Page
+ */
+ public function copyPage(Page $page, Entity $newParent, $newName = '')
+ {
+ $newBook = $newParent->isA('book') ? $newParent : $newParent->book;
+ $newChapter = $newParent->isA('chapter') ? $newParent : null;
+ $copyPage = $this->getDraftPage($newBook, $newChapter);
+ $pageData = $page->getAttributes();
+
+ // Update name
+ if (!empty($newName)) {
+ $pageData['name'] = $newName;
+ }
+
+ // Copy tags from previous page if set
+ if ($page->tags) {
+ $pageData['tags'] = [];
+ foreach ($page->tags as $tag) {
+ $pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
+ }
+ }
+
+ // Set priority
+ if ($newParent->isA('chapter')) {
+ $pageData['priority'] = $this->getNewChapterPriority($newParent);
+ } else {
+ $pageData['priority'] = $this->getNewBookPriority($newParent);
+ }
+
+ return $this->publishPageDraft($copyPage, $pageData);
+ }
+
/**
* Saves a page revision into the system.
* @param Page $page
return strip_tags($html);
}
- /**
- * Get a new draft page instance.
- * @param Book $book
- * @param Chapter|bool $chapter
- * @return Page
- */
- public function getDraftPage(Book $book, $chapter = false)
- {
- $page = $this->page->newInstance();
- $page->name = trans('entities.pages_initial_name');
- $page->created_by = user()->id;
- $page->updated_by = user()->id;
- $page->draft = true;
-
- if ($chapter) {
- $page->chapter_id = $chapter->id;
- }
-
- $book->pages()->save($page);
- $page = $this->page->find($page->id);
- $this->permissionService->buildJointPermissionsForEntity($page);
- return $page;
- }
-
/**
* Search for image usage within page content.
* @param $imageString
{
protected $entityRepo;
+ protected $imageService;
/**
* ExportService constructor.
* @param $entityRepo
*/
- public function __construct(EntityRepo $entityRepo)
+ public function __construct(EntityRepo $entityRepo, ImageService $imageService)
{
$this->entityRepo = $entityRepo;
+ $this->imageService = $imageService;
}
/**
* Includes required CSS & image content. Images are base64 encoded into the HTML.
* @param Page $page
* @return mixed|string
+ * @throws \Throwable
*/
public function pageToContainedHtml(Page $page)
{
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
+ * @throws \Throwable
*/
public function chapterToContainedHtml(Chapter $chapter)
{
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
+ * @throws \Throwable
*/
public function bookToContainedHtml(Book $book)
{
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
+ * @throws \Throwable
*/
public function pageToPdf(Page $page)
{
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
+ * @throws \Throwable
*/
public function chapterToPdf(Chapter $chapter)
{
* Convert a book to a PDF file
* @param Book $book
* @return string
+ * @throws \Throwable
*/
public function bookToPdf(Book $book)
{
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
+ * @throws \Exception
*/
protected function htmlToPdf($html)
{
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
foreach ($imageTagsOutput[0] as $index => $imgMatch) {
- $oldImgString = $imgMatch;
+ $oldImgTagString = $imgMatch;
$srcString = $imageTagsOutput[2][$index];
- $isLocal = strpos(trim($srcString), 'http') !== 0;
- if ($isLocal) {
- $pathString = public_path(trim($srcString, '/'));
- } else {
- $pathString = $srcString;
- }
-
- // Attempt to find local files even if url not absolute
- $base = baseUrl('/');
- if (strpos($srcString, $base) === 0) {
- $isLocal = true;
- $relString = str_replace($base, '', $srcString);
- $pathString = public_path(trim($relString, '/'));
- }
-
- if ($isLocal && !file_exists($pathString)) {
- continue;
- }
- try {
- if ($isLocal) {
- $imageContent = file_get_contents($pathString);
- } else {
- $ch = curl_init();
- curl_setopt_array($ch, [CURLOPT_URL => $pathString, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
- $imageContent = curl_exec($ch);
- $err = curl_error($ch);
- curl_close($ch);
- if ($err) {
- throw new \Exception("Image fetch failed, Received error: " . $err);
- }
- }
- $imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
- $newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
- } catch (\ErrorException $e) {
- $newImageString = '';
+ $imageEncoded = $this->imageService->imageUriToBase64($srcString);
+ if ($imageEncoded === null) {
+ $imageEncoded = $srcString;
}
- $htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
+ $newImgTagString = str_replace($srcString, $imageEncoded, $oldImgTagString);
+ $htmlContent = str_replace($oldImgTagString, $newImgTagString, $htmlContent);
}
}
return $image;
}
+ /**
+ * Convert a image URI to a Base64 encoded string.
+ * Attempts to find locally via set storage method first.
+ * @param string $uri
+ * @return null|string
+ * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
+ */
+ public function imageUriToBase64(string $uri)
+ {
+ $isLocal = strpos(trim($uri), 'http') !== 0;
+
+ // Attempt to find local files even if url not absolute
+ $base = baseUrl('/');
+ if (!$isLocal && strpos($uri, $base) === 0) {
+ $isLocal = true;
+ $uri = str_replace($base, '', $uri);
+ }
+
+ $imageData = null;
+
+ if ($isLocal) {
+ $uri = trim($uri, '/');
+ $storage = $this->getStorage();
+ if ($storage->exists($uri)) {
+ $imageData = $storage->get($uri);
+ }
+ } else {
+ try {
+ $ch = curl_init();
+ curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
+ $imageData = curl_exec($ch);
+ $err = curl_error($ch);
+ curl_close($ch);
+ if ($err) {
+ throw new \Exception("Image fetch failed, Received error: " . $err);
+ }
+ } catch (\Exception $e) {
+ }
+ }
+
+ if ($imageData === null) {
+ return null;
+ }
+
+ return 'data:image/' . pathinfo($uri, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageData);
+ }
+
/**
* Gets a public facing url for an image by checking relevant environment variables.
* @param string $filePath
/**
* Prepare the local entity cache and ensure it's empty
+ * @param Entity[] $entities
*/
- protected function readyEntityCache()
+ protected function readyEntityCache($entities = [])
{
- $this->entityCache = [
- 'books' => collect(),
- 'chapters' => collect()
- ];
+ $this->entityCache = [];
+
+ foreach ($entities as $entity) {
+ $type = $entity->getType();
+ if (!isset($this->entityCache[$type])) {
+ $this->entityCache[$type] = collect();
+ }
+ $this->entityCache[$type]->put($entity->id, $entity);
+ }
}
/**
*/
protected function getBook($bookId)
{
- if (isset($this->entityCache['books']) && $this->entityCache['books']->has($bookId)) {
- return $this->entityCache['books']->get($bookId);
+ if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
+ return $this->entityCache['book']->get($bookId);
}
$book = $this->book->find($bookId);
if ($book === null) {
$book = false;
}
- if (isset($this->entityCache['books'])) {
- $this->entityCache['books']->put($bookId, $book);
- }
return $book;
}
*/
protected function getChapter($chapterId)
{
- if (isset($this->entityCache['chapters']) && $this->entityCache['chapters']->has($chapterId)) {
- return $this->entityCache['chapters']->get($chapterId);
+ if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
+ return $this->entityCache['chapter']->get($chapterId);
}
$chapter = $this->chapter->find($chapterId);
if ($chapter === null) {
$chapter = false;
}
- if (isset($this->entityCache['chapters'])) {
- $this->entityCache['chapters']->put($chapterId, $chapter);
- }
return $chapter;
}
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
+ * @throws \Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
{
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
- $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
+ $this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
+ * @throws \Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param Entity[] $entities
+ * @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
{
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param array $roles
+ * @throws \Throwable
*/
protected function createManyJointPermissions($entities, $roles)
{
- $this->readyEntityCache();
+ $this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
- foreach ($role->getRelationValue('permissions') as $permission) {
+ foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
+ * @param string $action
* @return mixed
*/
- public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn)
+ public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
if ($this->isAdmin()) {
$this->clean();
return $query;
}
- $this->currentAction = 'view';
+ $this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @return array[int, Collection];
*/
- public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
+ public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
if (!in_array($entityType, $entityTypes)) {
continue;
}
- $search = $this->searchEntityTable($terms, $entityType, $page, $count);
- $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, true);
+ $search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
+ $entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
if ($entityTotal > $page * $count) {
$hasMore = true;
}
* @param string $entityType
* @param int $page
* @param int $count
+ * @param string $action
* @param bool $getCount Return the total count of the search
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
- public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
+ public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
{
- $query = $this->buildEntitySearchQuery($terms, $entityType);
+ $query = $this->buildEntitySearchQuery($terms, $entityType, $action);
if ($getCount) {
return $query->count();
}
* Create a search query for an entity
* @param array $terms
* @param string $entityType
+ * @param string $action
* @return \Illuminate\Database\Eloquent\Builder
*/
- protected function buildEntitySearchQuery($terms, $entityType = 'page')
+ protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
}
}
- return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
+ return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
* @param int $count
* @param int $page
* @param bool|false|array $filterModel
+ * @param string $action - used for permission checking
+ * @return
*/
- public function getPopular($count = 10, $page = 0, $filterModel = false)
+ public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
{
$skipCount = $count * $page;
- $query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type')
+ $query = $this->permissionService->filterRestrictedEntityRelations($this->view, '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');
"type": "project",
"require": {
"php": ">=7.0.0",
- "laravel/framework": "5.5.*",
+ "laravel/framework": "~5.5.22",
"fideloper/proxy": "~3.3",
"ext-tidy": "*",
"intervention/image": "^2.4",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "ed85d10e69b1071020178cb400a80e48",
+ "content-hash": "3bf33ab103b15b06ca06c85fd8ae3b78",
"packages": [
{
"name": "aws/aws-sdk-php",
- "version": "3.52.6",
+ "version": "3.56.4",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992"
+ "reference": "03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c9af7657eddc0267cc7ac4f969c10d5c18459992",
- "reference": "c9af7657eddc0267cc7ac4f969c10d5c18459992",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee",
+ "reference": "03273bb5c1d8098ff6c23b3fa9ee444c4cc1dcee",
"shasum": ""
},
"require": {
"s3",
"sdk"
],
- "time": "2018-02-09T22:53:37+00:00"
+ "time": "2018-05-18T19:53:15+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
},
{
"name": "egulias/email-validator",
- "version": "2.1.3",
+ "version": "2.1.4",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
- "reference": "1bec00a10039b823cc94eef4eddd47dcd3b2ca04"
+ "reference": "8790f594151ca6a2010c6218e09d96df67173ad3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/1bec00a10039b823cc94eef4eddd47dcd3b2ca04",
- "reference": "1bec00a10039b823cc94eef4eddd47dcd3b2ca04",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/8790f594151ca6a2010c6218e09d96df67173ad3",
+ "reference": "8790f594151ca6a2010c6218e09d96df67173ad3",
"shasum": ""
},
"require": {
},
"require-dev": {
"dominicsayers/isemail": "dev-master",
- "phpunit/phpunit": "^4.8.35",
+ "phpunit/phpunit": "^4.8.35||^5.7||^6.0",
"satooshi/php-coveralls": "^1.0.1"
},
"suggest": {
"validation",
"validator"
],
- "time": "2017-11-15T23:40:40+00:00"
+ "time": "2018-04-10T10:11:19+00:00"
},
{
"name": "erusev/parsedown",
- "version": "1.6.4",
+ "version": "1.7.1",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
- "reference": "fbe3fe878f4fe69048bb8a52783a09802004f548"
+ "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/erusev/parsedown/zipball/fbe3fe878f4fe69048bb8a52783a09802004f548",
- "reference": "fbe3fe878f4fe69048bb8a52783a09802004f548",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+ "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
"shasum": ""
},
"require": {
+ "ext-mbstring": "*",
"php": ">=5.3.0"
},
"require-dev": {
"markdown",
"parser"
],
- "time": "2017-11-14T20:44:03+00:00"
+ "time": "2018-03-08T01:11:30+00:00"
},
{
"name": "fideloper/proxy",
},
{
"name": "guzzlehttp/guzzle",
- "version": "6.3.0",
+ "version": "6.3.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699"
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699",
- "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba",
+ "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba",
"shasum": ""
},
"require": {
},
"require-dev": {
"ext-curl": "*",
- "phpunit/phpunit": "^4.0 || ^5.0",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
"psr/log": "^1.0"
},
"suggest": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "6.2-dev"
+ "dev-master": "6.3-dev"
}
},
"autoload": {
"rest",
"web service"
],
- "time": "2017-06-22T18:50:49+00:00"
+ "time": "2018-04-22T15:46:56+00:00"
},
{
"name": "guzzlehttp/promises",
},
{
"name": "laravel/framework",
- "version": "v5.5.34",
+ "version": "v5.5.40",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966"
+ "reference": "d724ce0aa61bbd9adf658215eec484f5dd6711d6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/1de7c0aec13eadbdddc2d1ba4019b064b2c6b966",
- "reference": "1de7c0aec13eadbdddc2d1ba4019b064b2c6b966",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/d724ce0aa61bbd9adf658215eec484f5dd6711d6",
+ "reference": "d724ce0aa61bbd9adf658215eec484f5dd6711d6",
"shasum": ""
},
"require": {
"doctrine/inflector": "~1.1",
- "erusev/parsedown": "~1.6",
+ "erusev/parsedown": "~1.7",
"ext-mbstring": "*",
"ext-openssl": "*",
- "league/flysystem": "~1.0",
+ "league/flysystem": "^1.0.8",
"monolog/monolog": "~1.12",
"mtdowling/cron-expression": "~1.0",
- "nesbot/carbon": "~1.20",
+ "nesbot/carbon": "^1.24.1",
"php": ">=7.0",
"psr/container": "~1.0",
"psr/simple-cache": "^1.0",
"illuminate/translation": "self.version",
"illuminate/validation": "self.version",
"illuminate/view": "self.version",
- "tightenco/collect": "self.version"
+ "tightenco/collect": "<5.5.33"
},
"require-dev": {
"aws/aws-sdk-php": "~3.0",
"framework",
"laravel"
],
- "time": "2018-02-06T15:36:55+00:00"
+ "time": "2018-03-30T13:29:30+00:00"
},
{
"name": "laravel/socialite",
- "version": "v3.0.9",
+ "version": "v3.0.11",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "fc1c8d415699e502f3e61cbc61e3250d5bd942eb"
+ "reference": "4d29ba66fdb38ec994b778e5e51657555cc10511"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/fc1c8d415699e502f3e61cbc61e3250d5bd942eb",
- "reference": "fc1c8d415699e502f3e61cbc61e3250d5bd942eb",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/4d29ba66fdb38ec994b778e5e51657555cc10511",
+ "reference": "4d29ba66fdb38ec994b778e5e51657555cc10511",
"shasum": ""
},
"require": {
"laravel",
"oauth"
],
- "time": "2017-11-06T16:02:48+00:00"
+ "time": "2018-05-12T17:44:53+00:00"
},
{
"name": "league/flysystem",
- "version": "1.0.42",
+ "version": "1.0.45",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "09eabc54e199950041aef258a85847676496fe8e"
+ "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/09eabc54e199950041aef258a85847676496fe8e",
- "reference": "09eabc54e199950041aef258a85847676496fe8e",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a99f94e63b512d75f851b181afcdf0ee9ebef7e6",
+ "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6",
"shasum": ""
},
"require": {
"sftp",
"storage"
],
- "time": "2018-01-27T16:03:56+00:00"
+ "time": "2018-05-07T08:44:23+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
- "version": "1.0.18",
+ "version": "1.0.19",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
- "reference": "dc09b19f455750663b922ed52dcc0ff215bed284"
+ "reference": "f135691ef6761542af301b7c9880f140fb12dc74"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/dc09b19f455750663b922ed52dcc0ff215bed284",
- "reference": "dc09b19f455750663b922ed52dcc0ff215bed284",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/f135691ef6761542af301b7c9880f140fb12dc74",
+ "reference": "f135691ef6761542af301b7c9880f140fb12dc74",
"shasum": ""
},
"require": {
}
],
"description": "Flysystem adapter for the AWS S3 SDK v3.x",
- "time": "2017-06-30T06:29:25+00:00"
+ "time": "2018-03-27T20:33:59+00:00"
},
{
"name": "league/oauth1-client",
},
{
"name": "nesbot/carbon",
- "version": "1.22.1",
+ "version": "1.27.0",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
- "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc"
+ "reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc",
- "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc",
+ "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/ef81c39b67200dcd7401c24363dcac05ac3a4fe9",
+ "reference": "ef81c39b67200dcd7401c24363dcac05ac3a4fe9",
"shasum": ""
},
"require": {
- "php": ">=5.3.0",
- "symfony/translation": "~2.6 || ~3.0"
+ "php": ">=5.3.9",
+ "symfony/translation": "~2.6 || ~3.0 || ~4.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2",
- "phpunit/phpunit": "~4.0 || ~5.0"
+ "phpunit/phpunit": "^4.8.35 || ^5.7"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.23-dev"
- }
- },
"autoload": {
"psr-4": {
- "Carbon\\": "src/Carbon/"
+ "": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"datetime",
"time"
],
- "time": "2017-01-16T07:55:07+00:00"
+ "time": "2018-04-23T09:02:57+00:00"
},
{
"name": "paragonie/random_compat",
- "version": "v2.0.11",
+ "version": "v2.0.12",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
- "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8"
+ "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8",
- "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
+ "reference": "258c89a6b97de7dfaf5b8c7607d0478e236b04fb",
"shasum": ""
},
"require": {
"pseudorandom",
"random"
],
- "time": "2017-09-27T21:40:39+00:00"
+ "time": "2018-04-04T21:24:14+00:00"
},
{
"name": "phenx/php-font-lib",
},
{
"name": "psr/simple-cache",
- "version": "1.0.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
- "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24"
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/753fa598e8f3b9966c886fe13f370baa45ef0e24",
- "reference": "753fa598e8f3b9966c886fe13f370baa45ef0e24",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
"shasum": ""
},
"require": {
"psr-16",
"simple-cache"
],
- "time": "2017-01-02T13:31:39+00:00"
+ "time": "2017-10-23T01:57:42+00:00"
},
{
"name": "ramsey/uuid",
},
{
"name": "socialiteproviders/gitlab",
- "version": "v3.0.1",
+ "version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/GitLab.git",
- "reference": "c96dc004563a3caf157608fe9aa9e45c79065d00"
+ "reference": "bab80e8e16853e062c58013b1c1f474bd5a5c49a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/c96dc004563a3caf157608fe9aa9e45c79065d00",
- "reference": "c96dc004563a3caf157608fe9aa9e45c79065d00",
+ "url": "https://api.github.com/repos/SocialiteProviders/GitLab/zipball/bab80e8e16853e062c58013b1c1f474bd5a5c49a",
+ "reference": "bab80e8e16853e062c58013b1c1f474bd5a5c49a",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
- "socialiteproviders/manager": "~3.0"
+ "socialiteproviders/manager": "~2.0 || ~3.0"
},
"type": "library",
"autoload": {
}
],
"description": "GitLab OAuth2 Provider for Laravel Socialite",
- "time": "2017-01-31T05:06:13+00:00"
+ "time": "2018-05-11T03:10:27+00:00"
},
{
"name": "socialiteproviders/manager",
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.7.0",
+ "version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b"
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/78be803ce01e55d3491c1397cf1c64beb9c1b63b",
- "reference": "78be803ce01e55d3491c1397cf1c64beb9c1b63b",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171",
+ "reference": "3296adf6a6454a050679cde90f95350ad604b171",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.7-dev"
+ "dev-master": "1.8-dev"
}
},
"autoload": {
"portable",
"shim"
],
- "time": "2018-01-30T19:27:44+00:00"
+ "time": "2018-04-26T10:06:28+00:00"
},
{
"name": "symfony/process",
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
- "version": "v3.1.1",
+ "version": "v3.1.4",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
- "reference": "f0018d359a2ad6968ad11b283283a925e017f3c9"
+ "reference": "7a91480cc6e597caed5117a3c5d685f06d35c5a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f0018d359a2ad6968ad11b283283a925e017f3c9",
- "reference": "f0018d359a2ad6968ad11b283283a925e017f3c9",
+ "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/7a91480cc6e597caed5117a3c5d685f06d35c5a1",
+ "reference": "7a91480cc6e597caed5117a3c5d685f06d35c5a1",
"shasum": ""
},
"require": {
"profiler",
"webprofiler"
],
- "time": "2018-02-07T08:29:09+00:00"
+ "time": "2018-03-06T08:35:31+00:00"
},
{
"name": "barryvdh/laravel-ide-helper",
},
{
"name": "mockery/mockery",
- "version": "1.0",
+ "version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/mockery/mockery.git",
- "reference": "1bac8c362b12f522fdd1f1fa3556284c91affa38"
+ "reference": "99e29d3596b16dabe4982548527d5ddf90232e99"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mockery/mockery/zipball/1bac8c362b12f522fdd1f1fa3556284c91affa38",
- "reference": "1bac8c362b12f522fdd1f1fa3556284c91affa38",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/99e29d3596b16dabe4982548527d5ddf90232e99",
+ "reference": "99e29d3596b16dabe4982548527d5ddf90232e99",
"shasum": ""
},
"require": {
"php": ">=5.6.0"
},
"require-dev": {
- "phpunit/phpunit": "~5.7|~6.1"
+ "phpdocumentor/phpdocumentor": "^2.9",
+ "phpunit/phpunit": "~5.7.10|~6.5"
},
"type": "library",
"extra": {
"homepage": "http://davedevelopment.co.uk"
}
],
- "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.",
- "homepage": "http://github.com/mockery/mockery",
+ "description": "Mockery is a simple yet flexible PHP mock object framework",
+ "homepage": "https://github.com/mockery/mockery",
"keywords": [
"BDD",
"TDD",
"test double",
"testing"
],
- "time": "2017-10-06T16:20:43+00:00"
+ "time": "2018-05-08T08:54:48+00:00"
},
{
"name": "myclabs/deep-copy",
},
{
"name": "phpspec/prophecy",
- "version": "1.7.3",
+ "version": "1.7.6",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf"
+ "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
- "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
+ "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
"phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
- "sebastian/comparator": "^1.1|^2.0",
+ "sebastian/comparator": "^1.1|^2.0|^3.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"require-dev": {
"phpspec/phpspec": "^2.5|^3.2",
- "phpunit/phpunit": "^4.8.35 || ^5.7"
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5"
},
"type": "library",
"extra": {
"spy",
"stub"
],
- "time": "2017-11-24T13:59:53+00:00"
+ "time": "2018-04-18T13:57:24+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "5.3.0",
+ "version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1"
+ "reference": "c89677919c5dd6d3b3852f230a663118762218ac"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1",
- "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
+ "reference": "c89677919c5dd6d3b3852f230a663118762218ac",
"shasum": ""
},
"require": {
"testing",
"xunit"
],
- "time": "2017-12-06T09:29:45+00:00"
+ "time": "2018-04-06T15:36:58+00:00"
},
{
"name": "phpunit/php-file-iterator",
},
{
"name": "phpunit/phpunit",
- "version": "6.5.6",
+ "version": "6.5.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe"
+ "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3330ef26ade05359d006041316ed0fa9e8e3cefe",
- "reference": "3330ef26ade05359d006041316ed0fa9e8e3cefe",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
+ "reference": "4f21a3c6b97c42952fd5c2837bb354ec0199b97b",
"shasum": ""
},
"require": {
"testing",
"xunit"
],
- "time": "2018-02-01T05:57:37+00:00"
+ "time": "2018-04-10T11:38:34+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
},
{
"name": "squizlabs/php_codesniffer",
- "version": "3.2.2",
+ "version": "3.2.3",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1"
+ "reference": "4842476c434e375f9d3182ff7b89059583aa8b27"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
- "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4842476c434e375f9d3182ff7b89059583aa8b27",
+ "reference": "4842476c434e375f9d3182ff7b89059583aa8b27",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
- "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
"bin/phpcs",
"phpcs",
"standards"
],
- "time": "2017-12-19T21:44:46+00:00"
+ "time": "2018-02-20T21:35:23+00:00"
},
{
"name": "symfony/class-loader",
|
*/
- 'domain' => null,
+ 'domain' => env('SESSION_DOMAIN', null),
/*
|--------------------------------------------------------------------------
|
*/
- 'secure' => false,
+ 'secure' => env('SESSION_SECURE_COOKIE', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTTP Access Only
+ |--------------------------------------------------------------------------
+ |
+ | Setting this value to true will prevent JavaScript from accessing the
+ | value of the cookie and the cookie will only be accessible through
+ | the HTTP protocol. You are free to modify this option if needed.
+ |
+ */
+
+ 'http_only' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Same-Site Cookies
+ |--------------------------------------------------------------------------
+ |
+ | This option determines how your cookies behave when cross-site requests
+ | take place, and can be used to mitigate CSRF attacks. By default, we
+ | do not enable this as other CSRF protection services are in place.
+ |
+ | Supported: "lax", "strict"
+ |
+ */
+
+ 'same_site' => null,
];
$role = \BookStack\Role::getRole('viewer');
$viewerUser->attachRole($role);
- factory(\BookStack\Book::class, 20)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
+ factory(\BookStack\Book::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($book) use ($editorUser) {
- $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
+ $chapters = factory(\BookStack\Chapter::class, 3)->create(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id])
->each(function($chapter) use ($editorUser, $book){
- $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
+ $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
});
$pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
--- /dev/null
+<?php
+
+use Illuminate\Database\Seeder;
+
+class LargeContentSeeder extends Seeder
+{
+ /**
+ * Run the database seeds.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ // Create an editor user
+ $editorUser = factory(\BookStack\User::class)->create();
+ $editorRole = \BookStack\Role::getRole('editor');
+ $editorUser->attachRole($editorRole);
+
+ $largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $editorUser->id, 'updated_by' => $editorUser->id]);
+ $largeBook->pages()->saveMany($pages);
+ $largeBook->chapters()->saveMany($chapters);
+ app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
+ app(\BookStack\Services\SearchService::class)->indexAllEntities();
+ }
+}
}
}
},
+ "@babel/polyfill": {
+ "version": "7.0.0-beta.46",
+ "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.0.0-beta.46.tgz",
+ "integrity": "sha512-eFFWNiI3Os7bBkIA10ZGBUMywK+1/OTVg+qsrlaXRBTpAN0n1g1pXCkNN4rcGpgLPNyfZHQEj+aVAyWPGerSIQ==",
+ "dev": true,
+ "requires": {
+ "core-js": "2.5.5",
+ "regenerator-runtime": "0.11.1"
+ },
+ "dependencies": {
+ "core-js": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
+ "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=",
+ "dev": true
+ },
+ "regenerator-runtime": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+ "dev": true
+ }
+ }
+ },
"@babel/preset-env": {
"version": "7.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.0.0-beta.40.tgz",
"babel-types": "6.26.0"
}
},
- "babel-polyfill": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
- "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=",
- "dev": true,
- "requires": {
- "babel-runtime": "6.26.0",
- "core-js": "2.5.1",
- "regenerator-runtime": "0.10.5"
- }
- },
"babel-preset-es2015": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz",
}
}
},
- "moment": {
- "version": "2.21.0",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
- "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ=="
- },
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
"regenerate": "1.3.3"
}
},
- "regenerator-runtime": {
- "version": "0.10.5",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
- "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=",
- "dev": true
- },
"regenerator-transform": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz",
}
},
"sass-loader": {
- "version": "6.0.7",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-6.0.7.tgz",
- "integrity": "sha512-JoiyD00Yo1o61OJsoP2s2kb19L1/Y2p3QFcCdWdF6oomBGKVYuZyqHWemRBfQ2uGYsk+CH3eCguXNfpjzlcpaA==",
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.0.1.tgz",
+ "integrity": "sha512-MeVVJFejJELlAbA7jrRchi88PGP6U9yIfqyiG+bBC4a9s2PX+ulJB9h8bbEohtPBfZmlLhNZ0opQM9hovRXvlw==",
"dev": true,
"requires": {
"clone-deep": "2.0.2",
}
},
"style-loader": {
- "version": "0.20.3",
- "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.20.3.tgz",
- "integrity": "sha512-2I7AVP73MvK33U7B9TKlYZAqdROyMXDYSMvHLX43qy3GCOaJNiV6i0v/sv9idWIaQ42Yn2dNv79Q5mKXbKhAZg==",
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.21.0.tgz",
+ "integrity": "sha512-T+UNsAcl3Yg+BsPKs1vd22Fr8sVT+CJMtzqc6LEw9bbJZb43lm9GoeIfUcDEefBSWC0BhYbcdupV1GtI4DGzxg==",
"dev": true,
"requires": {
"loader-utils": "1.1.0",
"@babel/preset-env": "^7.0.0-beta.40",
"autoprefixer": "^8.1.0",
"babel-loader": "^8.0.0-beta.0",
- "babel-polyfill": "^6.26.0",
+ "@babel/polyfill": "^7.0.0-beta.40",
"css-loader": "^0.28.10",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"livereload": "^0.7.0",
"node-sass": "^4.7.2",
"npm-run-all": "^4.1.2",
"postcss-loader": "^2.1.1",
- "sass-loader": "^6.0.7",
- "style-loader": "^0.20.3",
+ "sass-loader": "^7.0.1",
+ "style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.3",
"webpack": "^4.1.1",
"webpack-cli": "^2.0.11"
# BookStack
[](https://github.com/BookStackApp/BookStack/releases/latest)
-[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
+[](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[](https://travis-ci.org/BookStackApp/BookStack)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
--- /dev/null
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="M0 0h24v24H0z" fill="none"/>
+ <path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>
+ <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
-<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+<svg viewBox="0 0 24 24" fill="#b6531c" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
\ No newline at end of file
this.targetElem = document.getElementById('header');
this.showing = false;
this.breakPoint = 1200;
+
+ if (document.body.classList.contains('flexbox')) {
+ this.elem.style.display = 'none';
+ return;
+ }
+
this.elem.addEventListener('click', this.scrollToTop.bind(this));
window.addEventListener('scroll', this.onPageScroll.bind(this));
}
this.lastClick = 0;
let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
- this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`);
+ let entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
+ this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
this.input = elem.querySelector('[entity-selector-input]');
this.searchInput = elem.querySelector('[entity-selector-search]');
onClick(event) {
let t = event.target;
- console.log('click', t);
if (t.matches('.entity-list-item *')) {
event.preventDefault();
editor.addButton('drawio', {
tooltip: 'Drawing',
- image: window.baseUrl('/icon/drawing.svg?color=000000'),
+ image: `data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMDAwMDAiICB4bWxucz0iaHR0cDovL3d3 dy53My5vcmcvMjAwMC9zdmciPgogICAgPHBhdGggZD0iTTIzIDdWMWgtNnYySDdWMUgxdjZoMnYx MEgxdjZoNnYtMmgxMHYyaDZ2LTZoLTJWN2gyek0zIDNoMnYySDNWM3ptMiAxOEgzdi0yaDJ2Mnpt MTItMkg3di0ySDVWN2gyVjVoMTB2MmgydjEwaC0ydjJ6bTQgMmgtMnYtMmgydjJ6TTE5IDVWM2gy djJoLTJ6bS01LjI3IDloLTMuNDlsLS43MyAySDcuODlsMy40LTloMS40bDMuNDEgOWgtMS42M2wt Ljc0LTJ6bS0zLjA0LTEuMjZoMi42MUwxMiA4LjkxbC0xLjMxIDMuODN6Ii8+CiAgICA8cGF0aCBk PSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+Cjwvc3ZnPg==`,
cmd: 'drawio'
});
// Global Polyfills
-import "babel-polyfill"
+import "@babel/polyfill"
import "./services/dom-polyfills"
// Url retrieval function
const modeMap = {
css: 'css',
- c: 'clike',
- java: 'clike',
- scala: 'clike',
- kotlin: 'clike',
- 'c++': 'clike',
- 'c#': 'clike',
- csharp: 'clike',
+ c: 'text/x-csrc',
+ java: 'text/x-java',
+ scala: 'text/x-scala',
+ kotlin: 'text/x-kotlin',
+ 'c++': 'text/x-c++src',
+ 'c#': 'text/x-csharp',
+ csharp: 'text/x-csharp',
diff: 'diff',
go: 'go',
html: 'htmlmixed',
},
getFileUrl(file) {
+ if (file.external && file.path.indexOf('http') !== 0) {
+ return file.path;
+ }
return window.baseUrl(`/attachments/${file.id}`);
},
},
checkValidationErrors(groupName, err) {
- console.error(err);
- if (typeof err.response.data === "undefined" && typeof err.response.data.validation === "undefined") return;
- this.errors[groupName] = err.response.data.validation;
- console.log(this.errors[groupName]);
+ if (typeof err.response.data === "undefined" && typeof err.response.data === "undefined") return;
+ this.errors[groupName] = err.response.data;
},
getUploadUrl(file) {
attachNewLink(file) {
file.uploaded_to = this.pageId;
+ this.errors.link = {};
this.$http.post(window.baseUrl('/attachments/link'), file).then(resp => {
this.files.push(resp.data);
this.file = this.newFile();
lastSave = Date.now();
}, errorRes => {
if (draftErroring) return;
- window.$events('error', trans('errors.page_draft_autosave_fail'));
+ window.$events.emit('error', trans('errors.page_draft_autosave_fail'));
draftErroring = true;
});
},
display: block;
position: relative;
&:before {
- background-image: url("/icon/info-filled.svg?color=015380");
+ background-image: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMwMTUzODAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bTEgMTVoLTJ2LTZoMnY2em0wLThoLTJWN2gydjJ6Ii8+PC9zdmc+');
background-repeat: no-repeat;
content: '';
width: 1.2em;
color: darken($positive, 16%);
}
&.success:before {
- background-image: url("/icon/check-circle.svg?color=376c39");
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiMzNzZjMzkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6bS0yIDE1bC01LTUgMS40MS0xLjQxTDEwIDE0LjE3bDcuNTktNy41OUwxOSA4bC05IDl6Ii8+PC9zdmc+");
}
&.danger {
border-left-color: $negative;
color: darken($negative, 20%);
}
&.danger:before {
- background-image: url("/icon/danger.svg?color=b91818");
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiOTE4MTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0xNS43MyAzSDguMjdMMyA4LjI3djcuNDZMOC4yNyAyMWg3LjQ2TDIxIDE1LjczVjguMjdMMTUuNzMgM3pNMTIgMTcuM2MtLjcyIDAtMS4zLS41OC0xLjMtMS4zIDAtLjcyLjU4LTEuMyAxLjMtMS4zLjcyIDAgMS4zLjU4IDEuMyAxLjMgMCAuNzItLjU4IDEuMy0xLjMgMS4zem0xLTQuM2gtMlY3aDJ2NnoiLz4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==");
}
&.info {
border-left-color: $info;
color: darken($warning, 16%);
}
&.warning:before {
- background-image: url("/icon/warning.svg?color=b6531c");
+ background-image: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9IiNiNjUzMWMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+ICAgIDxwYXRoIGQ9Ik0wIDBoMjR2MjRIMHoiIGZpbGw9Im5vbmUiLz4gICAgPHBhdGggZD0iTTEgMjFoMjJMMTIgMiAxIDIxem0xMi0zaC0ydi0yaDJ2MnptMC00aC0ydi00aDJ2NHoiLz48L3N2Zz4=");
}
}
}
}
+.sidebar .card {
+ h3, .body, .empty-text {
+ padding: $-s $-m;
+ }
+}
+
.card.drag-card {
border: 1px solid #DDD;
border-radius: 4px;
padding: $-m;
border: 1px solid #DDD;
}
+
+.tag-item {
+ display: inline-flex;
+ margin-bottom: $-xs;
+ margin-right: $-xs;
+ border-radius: 4px;
+ border: 1px solid #CCC;
+ overflow: hidden;
+ font-size: 0.85em;
+ a, a:hover, a:active {
+ padding: 4px 8px;
+ color: #777;
+ transition: background-color ease-in-out 80ms;
+ text-decoration: none;
+ }
+ a:hover {
+ background-color: rgba(255, 255, 255, 0.7);
+ }
+ svg {
+ fill: #888;
+ }
+ .tag-value {
+ border-left: 1px solid #DDD;
+ background-color: rgba(255, 255, 255, 0.5);
+ }
+}
+
+.tag-list div:last-child .tag-item {
+ margin-bottom: 0;
+}
\ No newline at end of file
color: #666;
width: 250px;
max-width: 100%;
+
&.neg, &.invalid {
border: 1px solid $negative;
}
background-color: #F2F2F2;
max-width: 360px;
min-height: 90vh;
+ section {
+ margin: $-m;
+ }
}
.flex.sidebar + .flex.content {
flex: 3;
margin: 0;
}
}
+
+.card.entity-details {
+ .active-restriction {
+ margin-top: $-xs;
+ }
+ .active-restriction + .active-restriction {
+ margin-top: 0;
+ }
+}
\ No newline at end of file
}
}
+.blended-links a {
+ color: inherit;
+ svg {
+ fill: currentColor;
+ }
+}
+
/*
* Other HTML Text Elements
*/
'edit' => 'Edit',
'sort' => 'Sort',
'move' => 'Move',
+ 'copy' => 'Copy',
'reply' => 'Reply',
'delete' => 'Delete',
'search' => 'Search',
'pages_not_in_chapter' => 'Page is not in a chapter',
'pages_move' => 'Move Page',
'pages_move_success' => 'Page moved to ":parentName"',
+ 'pages_copy' => 'Copy Page',
+ 'pages_copy_desination' => 'Copy Destination',
+ 'pages_copy_success' => 'Page successfully copied',
'pages_permissions' => 'Page Permissions',
'pages_permissions_success' => 'Page permissions updated',
'pages_revision' => 'Revision',
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a page to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_default' => 'Default homepage view chosen',
+ 'app_homepage_books' => 'Or select the books page as your homepage. This will override any page selected as your homepage.',
'app_disable_comments' => 'Disable comments',
'app_disable_comments_desc' => 'Disable comments across all pages in the application. Existing comments are not shown.',
'edit' => 'Editar',
'sort' => 'Ordenar',
'move' => 'Mover',
+ 'copy' => 'Copiar',
'reply' => 'Responder',
'delete' => 'Borrar',
'search' => 'Buscar',
"grid_view" => "Vista de grilla",
"list_view" => "Vista de lista",
-
/**
* Header
*/
'image_upload_success' => 'Imagen subida éxitosamente',
'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
'image_delete_success' => 'Imagen borrada exitosamente',
+ 'image_upload_remove' => 'Quitar',
/**
* Code editor
'meta_created_name' => 'Creado el :timeLength por :user',
'meta_updated' => 'Actualizado el :timeLength',
'meta_updated_name' => 'Actualizado el :timeLength por :user',
- 'x_pages' => ':count Páginas',
'entity_select' => 'Seleccione entidad',
'images' => 'Imágenes',
'my_recent_drafts' => 'Mis borradores recientes',
'pages_not_in_chapter' => 'La página no esá en el capÃtulo',
'pages_move' => 'Mover página',
'pages_move_success' => 'Página movida a ":parentName"',
+ 'pages_copy' => 'Copiar página',
+ 'pages_copy_desination' => 'Destino de la copia',
+ 'pages_copy_success' => 'Página copiada con éxito',
'pages_permissions' => 'Permisos de página',
'pages_permissions_success' => 'Permisos de página actualizados',
'pages_revision' => 'Revisión',
* Editor sidebar
*/
'page_tags' => 'Etiquetas de página',
+ 'chapter_tags' => 'Etiquetas de capÃtulo',
+ 'book_tags' => 'Etiquetas de libro',
'tag' => 'Etiqueta',
'tags' => 'Etiquetas',
'tag_value' => 'Valor de la etiqueta (Opcional)',
*/
'comment' => 'Comentario',
'comments' => 'Comentarios',
+ 'comment_add' => 'Agregar comentario',
'comment_placeholder' => 'DEjar un comentario aquÃ',
'comment_count' => '{0} Sin Comentarios|{1} 1 Comentario|[2,*] :count Comentarios',
'comment_save' => 'Guardar comentario',
'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',
'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
+ 'uploaded' => 'El servidor no permite subir archivos de este tamaño. Por favor intente un tamaño menor.', 'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
'image_upload_type_error' => 'El tipo de imagen subida es inválido.',
</div>
</div>
-<div class="form-group" collapsible id="logo-control">
+<div class="form-group" collapsible id="tags-control">
<div class="collapse-title text-primary" collapsible-trigger>
- <label for="user-avatar">{{ trans('entities.book_tags') }}</label>
+ <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
</div>
<div class="collapse-content" collapsible-content>
@include('components.tag-manager', ['entity' => isset($book)?$book:null, 'entityType' => 'chapter'])
<p >{{ $book->getExcerpt(130) }}</p>
@endif
</div>
- <div class="grid-card-footer">
+ <div class="grid-card-footer text-muted text-small">
<span>@include('partials.entity-meta', ['entity' => $book])</span>
</div>
</div>
\ No newline at end of file
@section('toolbar')
<div class="col-xs-6">
<div class="action-buttons text-left">
- <form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-book-view") }}" method="POST" class="inline">
- {!! csrf_field() !!}
- {!! method_field('PATCH') !!}
- <input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="book_view_type">
- @if ($booksViewType === 'list')
- <button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button>
- @else
- <button type="submit" class="text-pos text-button">@icon('list'){{ trans('common.list_view') }}</button>
- @endif
- </form>
+ @include('books/view-toggle', ['booksViewType' => $booksViewType])
</div>
</div>
<div class="col-xs-6 faded">
@stop
@section('body')
- @if($booksViewType === 'list')
- <div class="container small" ng-non-bindable>
- @else
- <div class="container" ng-non-bindable>
- @endif
- <h1>{{ trans('entities.books') }}</h1>
- @if(count($books) > 0)
- @if($booksViewType === 'list')
- @foreach($books as $book)
- @include('books/list-item', ['book' => $book])
- <hr>
- @endforeach
- {!! $books->render() !!}
- @else
- <div class="grid third">
- @foreach($books as $key => $book)
- @include('books/grid-item', ['book' => $book])
- @endforeach
- </div>
- <div>
- {!! $books->render() !!}
- </div>
- @endif
- @else
- <p class="text-muted">{{ trans('entities.books_empty') }}</p>
- @if(userCan('books-create-all'))
- <a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_one_now') }}</a>
- @endif
- @endif
- </div>
+ @include('books/list', ['books' => $books, 'bookViewType' => $booksViewType])
@stop
\ No newline at end of file
--- /dev/null
+
+@if($booksViewType === 'list')
+ <div class="container small">
+@else
+ <div class="container">
+@endif
+ <h1>{{ trans('entities.books') }}</h1>
+ @if(count($books) > 0)
+ @if($booksViewType === 'list')
+ @foreach($books as $book)
+ @include('books/list-item', ['book' => $book])
+ <hr>
+ @endforeach
+ {!! $books->render() !!}
+ @else
+ <div class="grid third">
+ @foreach($books as $key => $book)
+ @include('books/grid-item', ['book' => $book])
+ @endforeach
+ </div>
+ <div>
+ {!! $books->render() !!}
+ </div>
+ @endif
+ @else
+ <p class="text-muted">{{ trans('entities.books_empty') }}</p>
+ @if(userCan('books-create-all'))
+ <a href="{{ baseUrl("/create-book") }}" class="text-pos">@icon('edit'){{ trans('entities.create_one_now') }}</a>
+ @endif
+ @endif
+</div>
\ No newline at end of file
@section('sidebar')
+ @if($book->tags->count() > 0)
+ <section>
+ @include('components.tag-list', ['entity' => $book])
+ </section>
+ @endif
+
<div class="card">
<div class="body">
<form v-on:submit.prevent="searchBook" class="search-box">
</div>
</div>
- @if($book->restricted)
- <div class="card">
- <h3>@icon('permission') {{ trans('entities.permissions') }}</h3>
- <div class="body">
- <p class="text-muted">
+ <div class="card entity-details">
+ <h3>@icon('info') {{ trans('common.details') }}</h3>
+ <div class="body text-small text-muted blended-links">
+ @include('partials.entity-meta', ['entity' => $book])
+ @if($book->restricted)
+ <div class="active-restriction">
@if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a>
@else
@icon('lock'){{ trans('entities.books_permissions_active') }}
@endif
- </p>
- </div>
- </div>
- @endif
-
- <div class="card">
- <h3>@icon('info') {{ trans('common.details') }}</h3>
- <div class="body">
- @include('partials.entity-meta', ['entity' => $book])
+ </div>
+ @endif
</div>
</div>
- @if($book->tags->count() > 0)
- <div class="card tag-display">
- <h3>@icon('tag') {{ trans('entities.book_tags') }}</h3>
- <div class="body">
- @include('components.tag-list', ['entity' => $book])
- </div>
- </div>
- @endif
-
@if(count($activity) > 0)
<div class="activity card">
<h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
--- /dev/null
+<form action="{{ baseUrl("/settings/users/{$currentUser->id}/switch-book-view") }}" method="POST" class="inline">
+ {!! csrf_field() !!}
+ {!! method_field('PATCH') !!}
+ <input type="hidden" value="{{ $booksViewType === 'list'? 'grid' : 'list' }}" name="book_view_type">
+ @if ($booksViewType === 'list')
+ <button type="submit" class="text-pos text-button">@icon('grid'){{ trans('common.grid_view') }}</button>
+ @else
+ <button type="submit" class="text-pos text-button">@icon('list'){{ trans('common.list_view') }}</button>
+ @endif
+</form>
\ No newline at end of file
@section('body')
- <div class="container">
+ <div class="container small">
<div class="card">
<h3>@icon('folder') {{ trans('entities.chapters_move') }}</h3>
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book'])
+ @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book', 'entityPermission' => 'chapter-create'])
<div class="form-group text-right">
<a href="{{ $chapter->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
@stop
@section('sidebar')
+
+ @if($chapter->tags->count() > 0)
+ <section>
+ @include('components.tag-list', ['entity' => $chapter])
+ </section>
+ @endif
+
<div class="card">
<div class="body">
<form @submit.prevent="searchBook" class="search-box">
</div>
</div>
- @if($book->restricted || $chapter->restricted)
- <div class="card">
- <h3>@icon('permission') {{ trans('entities.permissions') }}</h3>
- <div class="body">
- @if($book->restricted)
- <p class="text-muted">
- @if(userCan('restrictions-manage', $book))
- <a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a>
- @else
- @icon('lock'){{ trans('entities.books_permissions_active') }}
- @endif
- </p>
- @endif
-
- @if($chapter->restricted)
- <p class="text-muted">
- @if(userCan('restrictions-manage', $chapter))
- <a href="{{ $chapter->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.chapters_permissions_active') }}</a>
- @else
- @icon('lock'){{ trans('entities.chapters_permissions_active') }}
- @endif
- </p>
- @endif
- </div>
- </div>
- @endif
-
- <div class="card">
+ <div class="card entity-details">
<h3>@icon('info') {{ trans('common.details') }}</h3>
- <div class="body">
+ <div class="body blended-links text-small text-muted">
@include('partials.entity-meta', ['entity' => $chapter])
- </div>
- </div>
- @if($chapter->tags->count() > 0)
- <div class="card tag-display">
- <h3>@icon('tag') {{ trans('entities.chapter_tags') }}</h3>
- <div class="body">
- @include('components.tag-list', ['entity' => $chapter])
- </div>
+ @if($book->restricted)
+ <div class="active-restriction">
+ @if(userCan('restrictions-manage', $book))
+ <a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a>
+ @else
+ @icon('lock'){{ trans('entities.books_permissions_active') }}
+ @endif
+ </div>
+ @endif
+
+ @if($chapter->restricted)
+ <div class="active-restriction">
+ @if(userCan('restrictions-manage', $chapter))
+ <a href="{{ $chapter->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.chapters_permissions_active') }}</a>
+ @else
+ @icon('lock'){{ trans('entities.chapters_permissions_active') }}
+ @endif
+ </div>
+ @endif
</div>
- @endif
+ </div>
@include('partials/book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
@stop
--- /dev/null
+@extends('sidebar-layout')
+
+@section('toolbar')
+ <div class="col-sm-6 faded">
+ <div class="action-buttons text-left">
+ <a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
+ @include('books/view-toggle', ['booksViewType' => $booksViewType])
+ </div>
+ </div>
+@stop
+
+@section('sidebar')
+ @include('common/home-sidebar')
+@stop
+
+@section('body')
+ @include('books/list', ['books' => $books, 'bookViewType' => $booksViewType])
+@stop
\ No newline at end of file
--- /dev/null
+@extends('sidebar-layout')
+
+@section('toolbar')
+ <div class="col-sm-6 faded">
+ <div class="action-buttons text-left">
+ <a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
+ </div>
+ </div>
+@stop
+
+@section('sidebar')
+ @include('common/home-sidebar')
+@stop
+
+@section('body')
+ <div class="page-content" page-display="{{ $customHomepage->id }}">
+ @include('pages/page-display', ['page' => $customHomepage])
+ </div>
+@stop
--- /dev/null
+@if(count($draftPages) > 0)
+ <div id="recent-drafts" class="card">
+ <h3>@icon('edit') {{ trans('entities.my_recent_drafts') }}</h3>
+ @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
+ </div>
+@endif
+
+<div class="card">
+ <h3>@icon($signedIn ? 'view' : 'star-circle') {{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}</h3>
+ @include('partials/entity-list', [
+ 'entities' => $recents,
+ 'style' => 'compact',
+ 'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
+ ])
+</div>
+
+<div class="card">
+ <h3>@icon('file') <a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
+ <div id="recently-updated-pages">
+ @include('partials/entity-list', [
+ 'entities' => $recentlyUpdatedPages,
+ 'style' => 'compact',
+ 'emptyText' => trans('entities.no_pages_recently_updated')
+ ])
+ </div>
+</div>
+
+<div id="recent-activity" class="card">
+ <h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
+ @include('partials/activity-list', ['activity' => $activity])
+</div>
\ No newline at end of file
<div class="form-group">
- <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
+ <div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}" entity-permission="{{ $entityPermission or 'view' }}">
<input type="hidden" entity-selector-input name="{{$name}}" value="">
<input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
<div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
-<table>
- <tbody>
- @foreach($entity->tags as $tag)
- <tr class="tag">
- <td @if(!$tag->value) colspan="2" @endif><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">{{ $tag->name }}</a></td>
- @if($tag->value) <td class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></td> @endif
- </tr>
- @endforeach
- </tbody>
-</table>
\ No newline at end of file
+@foreach($entity->tags as $tag)
+ <div class="tag-item primary-background-light">
+ <div class="tag-name"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%5D') }}">@icon('tag'){{ $tag->name }}</a></div>
+ @if($tag->value) <div class="tag-value"><a href="{{ baseUrl('/search?term=%5B' . urlencode($tag->name) .'%3D' . urlencode($tag->value) . '%5D') }}">{{$tag->value}}</a></div> @endif
+ </div>
+@endforeach
\ No newline at end of file
+++ /dev/null
-@extends('sidebar-layout')
-
-@section('toolbar')
- <div class="col-sm-6 faded">
- <div class="action-buttons text-left">
- <a expand-toggle=".entity-list.compact .entity-item-snippet" class="text-primary text-button">@icon('expand-text'){{ trans('common.toggle_details') }}</a>
- </div>
- </div>
-@stop
-
-@section('sidebar')
- @if(count($draftPages) > 0)
- <div id="recent-drafts" class="card">
- <h3>@icon('edit') {{ trans('entities.my_recent_drafts') }}</h3>
- @include('partials/entity-list', ['entities' => $draftPages, 'style' => 'compact'])
- </div>
- @endif
-
- <div class="card">
- <h3>@icon($signedIn ? 'view' : 'star-circle') {{ trans('entities.' . ($signedIn ? 'my_recently_viewed' : 'books_recent')) }}</h3>
- @include('partials/entity-list', [
- 'entities' => $recents,
- 'style' => 'compact',
- 'emptyText' => $signedIn ? trans('entities.no_pages_viewed') : trans('entities.books_empty')
- ])
- </div>
-
- <div class="card">
- <h3>@icon('file') <a class="no-color" href="{{ baseUrl("/pages/recently-updated") }}">{{ trans('entities.recently_updated_pages') }}</a></h3>
- <div id="recently-updated-pages">
- @include('partials/entity-list', [
- 'entities' => $recentlyUpdatedPages,
- 'style' => 'compact',
- 'emptyText' => trans('entities.no_pages_recently_updated')
- ])
- </div>
- </div>
-
- <div id="recent-activity" class="card">
- <h3>@icon('time') {{ trans('entities.recent_activity') }}</h3>
- @include('partials/activity-list', ['activity' => $activity])
- </div>
-@stop
-
-@section('body')
- <div class="page-content" ng-non-bindable>
- @include('pages/page-display', ['page' => $customHomepage])
- </div>
-@stop
-
-@section('scripts')
- <script>
- setupPageShow({{$customHomepage->id}});
- </script>
-@stop
-
--- /dev/null
+@extends('simple-layout')
+
+@section('toolbar')
+ <div class="col-sm-12 faded">
+ @include('pages._breadcrumbs', ['page' => $page])
+ </div>
+@stop
+
+@section('body')
+
+ <div class="container small">
+ <p> </p>
+ <div class="card">
+ <h3>@icon('copy') {{ trans('entities.pages_copy') }}</h3>
+ <div class="body">
+ <form action="{{ $page->getUrl('/copy') }}" method="POST">
+ {!! csrf_field() !!}
+
+ <div class="form-group title-input">
+ <label for="name">{{ trans('common.name') }}</label>
+ @include('form/text', ['name' => 'name'])
+ </div>
+
+ <div class="form-group" collapsible>
+ <div class="collapse-title text-primary" collapsible-trigger>
+ <label for="entity_selection">{{ trans('entities.pages_copy_desination') }}</label>
+ </div>
+ <div class="collapse-content" collapsible-content>
+ @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
+ </div>
+ </div>
+
+
+ <div class="form-group text-right">
+ <a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
+ <button type="submit" class="button pos">{{ trans('entities.pages_copy') }}</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+
+@stop
<hr>
- @include('partials.entity-meta', ['entity' => $page])
+ <div class="text-muted text-small">
+ @include('partials.entity-meta', ['entity' => $page])
+ </div>
</div>
</div>
@section('body')
- <div class="container">
+ <div class="container small">
<p> </p>
<div class="card">
<h3>@icon('folder') {{ trans('entities.pages_move') }}</h3>
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter'])
+ @include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
<div class="form-group text-right">
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
@section('sidebar')
<div class="card">
<h3>@icon('info') {{ trans('common.details') }}</h3>
- <div class="body">
+ <div class="body text-small text-muted">
@include('partials.entity-meta', ['entity' => $revision])
</div>
</div>
<a dropdown-toggle class="text-primary text-button">@icon('more') {{ trans('common.more') }}</a>
<ul>
@if(userCan('page-update', $page))
+ <li><a href="{{ $page->getUrl('/copy') }}" class="text-primary" >@icon('copy'){{ trans('common.copy') }}</a></li>
<li><a href="{{ $page->getUrl('/move') }}" class="text-primary" >@icon('folder'){{ trans('common.move') }}</a></li>
<li><a href="{{ $page->getUrl('/revisions') }}" class="text-primary">@icon('history'){{ trans('entities.revisions') }}</a></li>
@endif
@stop
@section('sidebar')
- @if($book->restricted || ($page->chapter && $page->chapter->restricted) || $page->restricted)
- <div class="card">
- <h3>@icon('permission') {{ trans('entities.permissions') }}</h3>
- <div class="body">
- <div class="text-muted">
-
- @if($book->restricted)
- @if(userCan('restrictions-manage', $book))
- <a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a>
- @else
- @icon('lock'){{ trans('entities.books_permissions_active') }}
- @endif
- <br>
- @endif
-
- @if($page->chapter && $page->chapter->restricted)
- @if(userCan('restrictions-manage', $page->chapter))
- <a href="{{ $page->chapter->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.chapters_permissions_active') }}</a>
- @else
- @icon('lock'){{ trans('entities.chapters_permissions_active') }}
- @endif
- <br>
- @endif
-
- @if($page->restricted)
- @if(userCan('restrictions-manage', $page))
- <a href="{{ $page->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.pages_permissions_active') }}</a>
- @else
- @icon('lock'){{ trans('entities.pages_permissions_active') }}
- @endif
- <br>
- @endif
- </div>
- </div>
- </div>
- @endif
@if($page->tags->count() > 0)
- <div class="card tag-display">
- <h3>@icon('tag') {{ trans('entities.page_tags') }}</h3>
- <div class="body">
- @include('components.tag-list', ['entity' => $page])
- </div>
- </div>
+ <section>
+ @include('components.tag-list', ['entity' => $page])
+ </section>
@endif
@if ($page->attachments->count() > 0)
</div>
@endif
- <div class="card">
+ <div class="card entity-details">
<h3>@icon('info') {{ trans('common.details') }}</h3>
- <div class="body">
+ <div class="body text-muted text-small blended-links">
@include('partials.entity-meta', ['entity' => $page])
+
+ @if($book->restricted)
+ <div class="active-restriction">
+ @if(userCan('restrictions-manage', $book))
+ <a href="{{ $book->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.books_permissions_active') }}</a>
+ @else
+ @icon('lock'){{ trans('entities.books_permissions_active') }}
+ @endif
+ </div>
+ @endif
+
+ @if($page->chapter && $page->chapter->restricted)
+ <div class="active-restriction">
+ @if(userCan('restrictions-manage', $page->chapter))
+ <a href="{{ $page->chapter->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.chapters_permissions_active') }}</a>
+ @else
+ @icon('lock'){{ trans('entities.chapters_permissions_active') }}
+ @endif
+ </div>
+ @endif
+
+ @if($page->restricted)
+ <div class="active-restriction">
+ @if(userCan('restrictions-manage', $page))
+ <a href="{{ $page->getUrl('/permissions') }}">@icon('lock'){{ trans('entities.pages_permissions_active') }}</a>
+ @else
+ @icon('lock'){{ trans('entities.pages_permissions_active') }}
+ @endif
+ </div>
+ @endif
</div>
</div>
@stop
@section('body')
- <div class="page-content" page-display="{{ $page->id }}" ng-non-bindable>
+ <div class="page-content" page-display="{{ $page->id }}">
<div class="pointer-container" id="pointer">
<div class="pointer anim" >
-<p class="text-muted small">
+<div class="entity-meta">
@if($entity->isA('revision'))
- {{ trans('entities.pages_revision') }}
+ @icon('history'){{ trans('entities.pages_revision') }}
{{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
<br>
@endif
- @if ($entity->isA('page')) {{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @endif
+
+ @if ($entity->isA('page'))
+ @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
+ @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br>
+ @if (userCan('page-update', $entity))</a>@endif
+ @endif
+
+
@if ($entity->createdBy)
- {!! trans('entities.meta_created_name', [
+ @icon('star'){!! trans('entities.meta_created_name', [
'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"
]) !!}
@else
- <span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
+ @icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
@endif
+
<br>
+
@if ($entity->updatedBy)
- {!! trans('entities.meta_updated_name', [
+ @icon('edit'){!! trans('entities.meta_updated_name', [
'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"
]) !!}
@elseif (!$entity->isA('revision'))
- <span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
+ @icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
@endif
-</p>
\ No newline at end of file
+</div>
\ No newline at end of file
<label for="setting-app-homepage">{{ trans('settings.app_homepage') }}</label>
<p class="small">{{ trans('settings.app_homepage_desc') }}</p>
@include('components.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_default'), 'value' => setting('app-homepage')])
+ <p class="small">{{ trans('settings.app_homepage_books') }}</p>
+ @include('components.toggle-switch', ['name' => 'setting-app-book-homepage', 'value' => setting('app-book-homepage')])
</div>
</div>
<?php
Route::get('/translations', 'HomeController@getTranslations');
-Route::get('/icon/{iconName}.svg', 'HomeController@getIcon');
Route::get('/robots.txt', 'HomeController@getRobots');
// Authenticated routes...
Route::get('/{bookSlug}/page/{pageSlug}/edit', 'PageController@edit');
Route::get('/{bookSlug}/page/{pageSlug}/move', 'PageController@showMove');
Route::put('/{bookSlug}/page/{pageSlug}/move', 'PageController@move');
+ Route::get('/{bookSlug}/page/{pageSlug}/copy', 'PageController@showCopy');
+ Route::post('/{bookSlug}/page/{pageSlug}/copy', 'PageController@copy');
Route::get('/{bookSlug}/page/{pageSlug}/delete', 'PageController@showDelete');
Route::get('/{bookSlug}/draft/{pageId}/delete', 'PageController@showDeleteDraft');
Route::get('/{bookSlug}/page/{pageSlug}/permissions', 'PageController@showRestrict');
// Password reset routes...
Route::get('/password/reset/{token}', 'Auth\ResetPasswordController@showResetForm');
-Route::post('/password/reset', 'Auth\ResetPasswordController@reset');
\ No newline at end of file
+Route::post('/password/reset', 'Auth\ResetPasswordController@reset');
+
+Route::fallback('HomeController@getNotFound');
\ No newline at end of file
{
use DatabaseTransactions;
-
- // Local user instances
- private $admin;
- private $editor;
+ use SharedTestHelpers;
/**
* The base URL to use while testing the application.
return $app;
}
- /**
- * Set the current user context to be an admin.
- * @return $this
- */
- public function asAdmin()
- {
- return $this->actingAs($this->getAdmin());
- }
-
- /**
- * Get the current admin user.
- * @return mixed
- */
- public function getAdmin() {
- if($this->admin === null) {
- $adminRole = Role::getSystemRole('admin');
- $this->admin = $adminRole->users->first();
- }
- return $this->admin;
- }
-
- /**
- * Set the current editor context to be an editor.
- * @return $this
- */
- public function asEditor()
- {
- if ($this->editor === null) {
- $this->editor = $this->getEditor();
- }
- return $this->actingAs($this->editor);
- }
/**
* Get a user that's not a system user such as the guest user.
$restrictionService->buildJointPermissionsForEntity($entity);
}
- /**
- * Get an instance of a user with 'editor' permissions
- * @param array $attributes
- * @return mixed
- */
- protected function getEditor($attributes = [])
- {
- $user = \BookStack\Role::getRole('editor')->users()->first();
- if (!empty($attributes)) $user->forceFill($attributes)->save();
- return $user;
- }
-
- /**
- * Get an instance of a user with 'viewer' permissions
- * @return mixed
- */
- protected function getViewer()
- {
- $user = \BookStack\Role::getRole('viewer')->users()->first();
- if (!empty($attributes)) $user->forceFill($attributes)->save();
- return $user;
- }
/**
* Quick way to create a new user without any permissions
use BookStack\Page;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
+use Carbon\Carbon;
class EntityTest extends BrowserKitTest
{
public function test_recently_updated_pages_on_home()
{
$page = Page::orderBy('updated_at', 'asc')->first();
+ Page::where('id', '!=', $page->id)->update([
+ 'updated_at' => Carbon::now()->subSecond(1)
+ ]);
$this->asAdmin()->visit('/')
->dontSeeInElement('#recently-updated-pages', $page->name);
$this->visit($page->getUrl() . '/edit')
<?php namespace Tests;
use BookStack\Book;
+use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
public function setUp()
{
parent::setUp();
- $this->book = \BookStack\Book::first();
+ $this->book = Book::first();
}
public function test_drafts_do_not_show_up()
public function test_page_move()
{
- $page = \BookStack\Page::first();
+ $page = Page::first();
$currentBook = $page->book;
- $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
+ $newBook = Book::where('id', '!=', $currentBook->id)->first();
- $resp = $this->asAdmin()->get($page->getUrl() . '/move');
+ $resp = $this->asEditor()->get($page->getUrl('/move'));
$resp->assertSee('Move Page');
- $movePageResp = $this->put($page->getUrl() . '/move', [
+ $movePageResp = $this->put($page->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
- $page = \BookStack\Page::find($page->id);
+ $page = Page::find($page->id);
$movePageResp->assertRedirect($page->getUrl());
$this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
$newBookResp->assertSee($page->name);
}
+ public function test_page_move_requires_create_permissions_on_parent()
+ {
+ $page = Page::first();
+ $currentBook = $page->book;
+ $newBook = Book::where('id', '!=', $currentBook->id)->first();
+ $editor = $this->getEditor();
+
+ $this->setEntityRestrictions($newBook, ['view', 'edit', 'delete'], $editor->roles);
+
+ $movePageResp = $this->actingAs($editor)->put($page->getUrl('/move'), [
+ 'entity_selection' => 'book:' . $newBook->id
+ ]);
+ $this->assertPermissionError($movePageResp);
+
+ $this->setEntityRestrictions($newBook, ['view', 'edit', 'delete', 'create'], $editor->roles);
+ $movePageResp = $this->put($page->getUrl('/move'), [
+ 'entity_selection' => 'book:' . $newBook->id
+ ]);
+
+ $page = Page::find($page->id);
+ $movePageResp->assertRedirect($page->getUrl());
+
+ $this->assertTrue($page->book->id == $newBook->id, 'Page book is now the new book');
+ }
+
public function test_chapter_move()
{
- $chapter = \BookStack\Chapter::first();
+ $chapter = Chapter::first();
$currentBook = $chapter->book;
$pageToCheck = $chapter->pages->first();
- $newBook = \BookStack\Book::where('id', '!=', $currentBook->id)->first();
+ $newBook = Book::where('id', '!=', $currentBook->id)->first();
- $chapterMoveResp = $this->asAdmin()->get($chapter->getUrl() . '/move');
+ $chapterMoveResp = $this->asEditor()->get($chapter->getUrl('/move'));
$chapterMoveResp->assertSee('Move Chapter');
- $moveChapterResp = $this->put($chapter->getUrl() . '/move', [
+ $moveChapterResp = $this->put($chapter->getUrl('/move'), [
'entity_selection' => 'book:' . $newBook->id
]);
- $chapter = \BookStack\Chapter::find($chapter->id);
+ $chapter = Chapter::find($chapter->id);
$moveChapterResp->assertRedirect($chapter->getUrl());
$this->assertTrue($chapter->book->id === $newBook->id, 'Chapter Book is now the new book');
$newBookResp->assertSee('moved chapter');
$newBookResp->assertSee($chapter->name);
- $pageToCheck = \BookStack\Page::find($pageToCheck->id);
+ $pageToCheck = Page::find($pageToCheck->id);
$this->assertTrue($pageToCheck->book_id === $newBook->id, 'Chapter child page\'s book id has changed to the new book');
$pageCheckResp = $this->get($pageToCheck->getUrl());
$pageCheckResp->assertSee($newBook->name);
];
}
- $sortResp = $this->asAdmin()->put($newBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]);
+ $sortResp = $this->asEditor()->put($newBook->getUrl() . '/sort', ['sort-tree' => json_encode($reqData)]);
$sortResp->assertRedirect($newBook->getUrl());
$sortResp->assertStatus(302);
$this->assertDatabaseHas('chapters', [
$checkResp->assertSee($newBook->name);
}
+ public function test_page_copy()
+ {
+ $page = Page::first();
+ $currentBook = $page->book;
+ $newBook = Book::where('id', '!=', $currentBook->id)->first();
+
+ $resp = $this->asEditor()->get($page->getUrl('/copy'));
+ $resp->assertSee('Copy Page');
+
+ $movePageResp = $this->post($page->getUrl('/copy'), [
+ 'entity_selection' => 'book:' . $newBook->id,
+ 'name' => 'My copied test page'
+ ]);
+
+ $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+ $movePageResp->assertRedirect($pageCopy->getUrl());
+ $this->assertTrue($pageCopy->book->id == $newBook->id, 'Page was copied to correct book');
+ }
+
+ public function test_page_copy_with_no_destination()
+ {
+ $page = Page::first();
+ $currentBook = $page->book;
+
+ $resp = $this->asEditor()->get($page->getUrl('/copy'));
+ $resp->assertSee('Copy Page');
+
+ $movePageResp = $this->post($page->getUrl('/copy'), [
+ 'name' => 'My copied test page'
+ ]);
+
+ $pageCopy = Page::where('name', '=', 'My copied test page')->first();
+
+ $movePageResp->assertRedirect($pageCopy->getUrl());
+ $this->assertTrue($pageCopy->book->id == $currentBook->id, 'Page was copied to correct book');
+ $this->assertTrue($pageCopy->id !== $page->id, 'Page copy is not the same instance');
+ }
+
}
\ No newline at end of file
$homeVisit->assertSee($name);
$homeVisit->assertStatus(200);
}
+
+ public function test_set_book_homepage()
+ {
+ $editor = $this->getEditor();
+ setting()->putUser($editor, 'books_view_type', 'grid');
+
+ $this->setSettings(['app-book-homepage' => true]);
+
+ $this->asEditor();
+ $homeVisit = $this->get('/');
+ $homeVisit->assertSee('Books');
+ $homeVisit->assertSee('book-grid-item grid-card');
+ $homeVisit->assertSee('grid-card-content');
+ $homeVisit->assertSee('grid-card-footer');
+ $homeVisit->assertSee('featured-image-container');
+
+ $this->setSettings(['app-book-homepage' => false]);
+ $this->test_default_homepage_visible();
+ }
}
}
}
+ public function test_secure_images_included_in_exports()
+ {
+ config()->set('filesystems.default', 'local_secure');
+ $this->asEditor();
+ $galleryFile = $this->getTestImage('my-secure-test-upload');
+ $page = Page::first();
+ $expectedPath = storage_path('uploads/images/gallery/' . Date('Y-m-M') . '/my-secure-test-upload');
+
+ $upload = $this->call('POST', '/images/gallery/upload', ['uploaded_to' => $page->id], [], ['file' => $galleryFile], []);
+ $imageUrl = json_decode($upload->getContent(), true)['url'];
+ $page->html .= "<img src=\"{$imageUrl}\">";
+ $page->save();
+ $upload->assertStatus(200);
+
+ $encodedImageContent = base64_encode(file_get_contents($expectedPath));
+ $export = $this->get($page->getUrl('/export/html'));
+ $this->assertTrue(str_contains($export->getContent(), $encodedImageContent), 'Uploaded image in export content');
+
+ if (file_exists($expectedPath)) {
+ unlink($expectedPath);
+ }
+ }
+
public function test_system_images_remain_public()
{
config()->set('filesystems.default', 'local_secure');
<?php namespace Tests;
use BookStack\Book;
-use BookStack\Services\PermissionService;
+use BookStack\Entity;
use BookStack\User;
use BookStack\Repos\EntityRepo;
*/
protected $viewer;
- /**
- * @var PermissionService
- */
- protected $permissionService;
-
public function setUp()
{
parent::setUp();
$this->user = $this->getEditor();
$this->viewer = $this->getViewer();
- $this->permissionService = $this->app[PermissionService::class];
}
- /**
- * Manually set some permissions on an entity.
- * @param \BookStack\Entity $entity
- * @param $actions
- */
- protected function setEntityRestrictions(\BookStack\Entity $entity, $actions)
+ protected function setEntityRestrictions(Entity $entity, $actions = [], $roles = [])
{
- $entity->restricted = true;
- $entity->permissions()->delete();
-
- $role = $this->user->roles->first();
- $viewerRole = $this->viewer->roles->first();
-
- $permissions = [];
- foreach ($actions as $action) {
- $permissions[] = [
- 'role_id' => $role->id,
- 'action' => strtolower($action)
- ];
- $permissions[] = [
- 'role_id' => $viewerRole->id,
- 'action' => strtolower($action)
- ];
- }
- $entity->permissions()->createMany($permissions);
-
- $entity->save();
- $entity->load('permissions');
- $this->permissionService->buildJointPermissionsForEntity($entity);
- $entity->load('jointPermissions');
+ $roles = [
+ $this->user->roles->first(),
+ $this->viewer->roles->first(),
+ ];
+ parent::setEntityRestrictions($entity, $actions, $roles);
}
public function test_book_view_restriction()
$this->user = $this->getViewer();
}
- protected function getViewer()
- {
- $role = \BookStack\Role::getRole('viewer');
- $viewer = $this->getNewBlankUser();
- $viewer->attachRole($role);;
- return $viewer;
- }
-
/**
* Give the given user some permissions.
* @param \BookStack\User $user
--- /dev/null
+<?php namespace Tests;
+
+use BookStack\Book;
+use BookStack\Chapter;
+use BookStack\Entity;
+use BookStack\Repos\EntityRepo;
+use BookStack\Role;
+use BookStack\Services\PermissionService;
+use BookStack\Services\SettingService;
+
+trait SharedTestHelpers
+{
+
+ protected $admin;
+ protected $editor;
+
+ /**
+ * Set the current user context to be an admin.
+ * @return $this
+ */
+ public function asAdmin()
+ {
+ return $this->actingAs($this->getAdmin());
+ }
+
+ /**
+ * Get the current admin user.
+ * @return mixed
+ */
+ public function getAdmin() {
+ if($this->admin === null) {
+ $adminRole = Role::getSystemRole('admin');
+ $this->admin = $adminRole->users->first();
+ }
+ return $this->admin;
+ }
+
+ /**
+ * Set the current user context to be an editor.
+ * @return $this
+ */
+ public function asEditor()
+ {
+ return $this->actingAs($this->getEditor());
+ }
+
+
+ /**
+ * Get a editor user.
+ * @return mixed
+ */
+ protected function getEditor() {
+ if($this->editor === null) {
+ $editorRole = Role::getRole('editor');
+ $this->editor = $editorRole->users->first();
+ }
+ return $this->editor;
+ }
+
+ /**
+ * Get an instance of a user with 'viewer' permissions
+ * @param $attributes
+ * @return mixed
+ */
+ protected function getViewer($attributes = [])
+ {
+ $user = \BookStack\Role::getRole('viewer')->users()->first();
+ if (!empty($attributes)) $user->forceFill($attributes)->save();
+ return $user;
+ }
+
+ /**
+ * Create and return a new book.
+ * @param array $input
+ * @return Book
+ */
+ public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
+ return $this->app[EntityRepo::class]->createFromInput('book', $input, false);
+ }
+
+ /**
+ * Create and return a new test chapter
+ * @param array $input
+ * @param Book $book
+ * @return Chapter
+ */
+ public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
+ return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
+ }
+
+ /**
+ * Create and return a new test page
+ * @param array $input
+ * @return Chapter
+ */
+ public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
+ $book = Book::first();
+ $entityRepo = $this->app[EntityRepo::class];
+ $draftPage = $entityRepo->getDraftPage($book);
+ return $entityRepo->publishPageDraft($draftPage, $input);
+ }
+
+ /**
+ * Quickly sets an array of settings.
+ * @param $settingsArray
+ */
+ protected function setSettings($settingsArray)
+ {
+ $settings = app(SettingService::class);
+ foreach ($settingsArray as $key => $value) {
+ $settings->put($key, $value);
+ }
+ }
+
+ /**
+ * Manually set some permissions on an entity.
+ * @param Entity $entity
+ * @param array $actions
+ * @param array $roles
+ */
+ protected function setEntityRestrictions(Entity $entity, $actions = [], $roles = [])
+ {
+ $entity->restricted = true;
+ $entity->permissions()->delete();
+
+ $permissions = [];
+ foreach ($actions as $action) {
+ foreach ($roles as $role) {
+ $permissions[] = [
+ 'role_id' => $role->id,
+ 'action' => strtolower($action)
+ ];
+ }
+ }
+ $entity->permissions()->createMany($permissions);
+
+ $entity->save();
+ $entity->load('permissions');
+ $this->app[PermissionService::class]->buildJointPermissionsForEntity($entity);
+ $entity->load('jointPermissions');
+ }
+
+}
\ No newline at end of file
<?php namespace Tests;
-use BookStack\Book;
-use BookStack\Chapter;
-use BookStack\Repos\EntityRepo;
-use BookStack\Role;
-use BookStack\Services\SettingService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
+use Illuminate\Foundation\Testing\TestResponse;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use DatabaseTransactions;
-
- protected $admin;
- protected $editor;
-
+ use SharedTestHelpers;
/**
* The base URL to use while testing the application.
* @var string
protected $baseUrl = 'http://localhost';
/**
- * Set the current user context to be an admin.
- * @return $this
- */
- public function asAdmin()
- {
- return $this->actingAs($this->getAdmin());
- }
-
- /**
- * Get the current admin user.
- * @return mixed
- */
- public function getAdmin() {
- if($this->admin === null) {
- $adminRole = Role::getSystemRole('admin');
- $this->admin = $adminRole->users->first();
- }
- return $this->admin;
- }
-
- /**
- * Set the current user context to be an editor.
- * @return $this
- */
- public function asEditor()
- {
- return $this->actingAs($this->getEditor());
- }
-
-
- /**
- * Get a editor user.
- * @return mixed
- */
- public function getEditor() {
- if($this->editor === null) {
- $editorRole = Role::getRole('editor');
- $this->editor = $editorRole->users->first();
- }
- return $this->editor;
- }
-
- /**
- * Get an instance of a user with 'viewer' permissions
- * @param $attributes
- * @return mixed
- */
- protected function getViewer($attributes = [])
- {
- $user = \BookStack\Role::getRole('viewer')->users()->first();
- if (!empty($attributes)) $user->forceFill($attributes)->save();
- return $user;
- }
-
- /**
- * Create and return a new book.
- * @param array $input
- * @return Book
- */
- public function newBook($input = ['name' => 'test book', 'description' => 'My new test book']) {
- return $this->app[EntityRepo::class]->createFromInput('book', $input, false);
- }
-
- /**
- * Create and return a new test chapter
- * @param array $input
- * @param Book $book
- * @return Chapter
- */
- public function newChapter($input = ['name' => 'test chapter', 'description' => 'My new test chapter'], Book $book) {
- return $this->app[EntityRepo::class]->createFromInput('chapter', $input, $book);
- }
-
- /**
- * Create and return a new test page
- * @param array $input
- * @return Chapter
- */
- public function newPage($input = ['name' => 'test page', 'html' => 'My new test page']) {
- $book = Book::first();
- $entityRepo = $this->app[EntityRepo::class];
- $draftPage = $entityRepo->getDraftPage($book);
- return $entityRepo->publishPageDraft($draftPage, $input);
- }
-
- /**
- * Quickly sets an array of settings.
- * @param $settingsArray
+ * Assert a permission error has occurred.
+ * @param TestResponse $response
*/
- protected function setSettings($settingsArray)
+ protected function assertPermissionError(TestResponse $response)
{
- $settings = app(SettingService::class);
- foreach ($settingsArray as $key => $value) {
- $settings->put($key, $value);
- }
+ $response->assertRedirect('/');
+ $this->assertTrue(session()->has('error'));
+ session()->remove('error');
}
}
\ No newline at end of file