Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1
FROM ghcr.io/linuxserver/baseimage-alpine-nginx:3.23

# set version label
LABEL build_version="Metron Security version:- 1.0"
LABEL maintainer="Metron Security"

ENV S6_STAGE2_HOOK="/init-hook"

RUN \
echo "**** install runtime packages ****" && \
apk add --no-cache \
fontconfig \
mariadb-client \
memcached \
php85-dom \
php85-exif \
php85-gd \
php85-ldap \
php85-mysqlnd \
php85-pdo_mysql \
php85-pecl-memcached \
php85-tokenizer \
qt5-qtbase \
ttf-freefont && \
echo "**** configure php-fpm to pass env vars ****" && \
sed -E -i 's/^;?clear_env ?=.*$/clear_env = no/g' /etc/php85/php-fpm.d/www.conf && \
if ! grep -qxF 'clear_env = no' /etc/php85/php-fpm.d/www.conf; then echo 'clear_env = no' >> /etc/php85/php-fpm.d/www.conf; fi && \
echo "env[PATH] = /usr/local/bin:/usr/bin:/bin" >> /etc/php85/php-fpm.conf && \
echo "**** preparing application directory ****" && \
mkdir -p /app/www

# --- CRITICAL CHANGE: Copy files before running composer ---
COPY . /app/www/

RUN \
echo "**** install composer dependencies ****" && \
cd /app/www && \
composer install --no-dev && \
echo "**** create symlinks for linuxserver persistence ****" && \
/bin/bash -c \
'dst=(www/themes www/files www/images www/uploads backups www/framework/cache www/framework/sessions www/framework/views www/framework/purifier log/bookstack/laravel.log www/.env); \
src=(themes storage/uploads/files storage/uploads/images public/uploads storage/backups storage/framework/cache storage/framework/sessions storage/framework/views storage/framework/purifier storage/logs/laravel.log .env); \
for i in "${!src[@]}"; do \
rm -rf /app/www/"${src[i]}" && \
ln -s /config/"${dst[i]}" /app/www/"${src[i]}"; \
done' && \
echo "**** cleanup ****" && \
rm -rf \
/tmp/* \
$HOME/.cache \
$HOME/.composer

# copy linuxserver init scripts (if you have them in a root/ folder)
# COPY root/ /

# Set ownership to the linuxserver default user
RUN chown -R abc:abc /app/www

EXPOSE 80 443
VOLUME /config
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions public/custom_slack_notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
document.addEventListener("DOMContentLoaded", function () {
const notifyButton = document.getElementById("slack-notify-button");

if (!notifyButton) return;

notifyButton.addEventListener("click", function (event) {
event.preventDefault();
notifyButton.innerHTML =
'<i class="fa fa-spinner fa-spin"></i> Completing...';

// Get data from the button's data attributes
const pageName = document.title;
const pageUrl = window.location.href;
const csrfToken = document.querySelector('meta[name="token"]').content;

notifyButton.disabled = true;

fetch("/ajax/training-complete", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken,
},
body: JSON.stringify({
page_name: pageName,
page_url: pageUrl,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.status === "ok") {
alert("Chapter successfully completed!");
notifyButton.innerHTML = "Completed";
} else {
alert("Error sending notification.");
notifyButton.disabled = false;
notifyButton.innerHTML = "Mark as Complete";
}
})
.catch((error) => {
console.error("Error:", error);
notifyButton.disabled = false;
notifyButton.innerHTML = "Mark as Complete";
});
});
});
3 changes: 3 additions & 0 deletions themes/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
*
!.gitignore

!my-org-theme/
!my-org-theme/**
33 changes: 33 additions & 0 deletions themes/my-org-theme/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

BookStack\Facades\Theme::listen('app_boot', function () {

Route::post('/ajax/training-complete', function (Request $request) {

if (!$request->has(['page_name', 'page_url'])) {
return response()->json(['status' => 'error', 'message' => 'Missing data'], 400);
}

$webhookUrl = config('services.slack.webhook_url') ?: env('SLACK_WEBHOOK_URL');

if (!$webhookUrl) {
return response()->json(['status' => 'error', 'message' => 'Webhook missing'], 500);
}

$payload = [
"text" => ":blue_book: *Training Completed!* :blue_book:\n" .
"*User:* " . auth()->user()->name . "\n" .
"*Page:* <" . $request->page_url . "|" . $request->page_name . ">\n" .
"*Time:* " . now()->setTimezone('Asia/Kolkata')->format('Y-m-d h:i:s A')
];

$response = Http::post($webhookUrl, $payload);

return response()->json(['status' => $response->successful() ? 'ok' : 'error']);
})->middleware(['web', 'auth']);

});
208 changes: 208 additions & 0 deletions themes/my-org-theme/pages/show.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
@extends('layouts.tri')

@push('social-meta')
<meta property="og:description" content="{{ Str::limit($page->text, 100, '...') }}">
@endpush

@include('entities.body-tag-classes', ['entity' => $page])

@section('body')

<div class="mb-m print-hidden">
@include('entities.breadcrumbs', ['crumbs' => [
$page->book,
$page->hasChapter() ? $page->chapter : null,
$page,
]])
</div>

<main class="content-wrap card">
<div component="page-display"
option:page-display:page-id="{{ $page->id }}"
class="page-content clearfix">
@include('pages.parts.page-display')
</div>

@if(auth()->check())
<div style="display:flex; justify-content:flex-end; margin-top: 20px;">
<button id="slack-notify-button"
class="button outline small"
type="button"
style="background-color:#20134c; color: #fff;"
data-page-name="{{ $page->name }}"
data-page-url="{{ $page->getUrl() }}">
@icon('send') Mark as Complete
</button>
</div>
@endif

@include('pages.parts.pointer', ['page' => $page])
</main>

@include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous])

@if ($commentTree->enabled())
<div class="comments-container mb-l print-hidden">
@include('comments.comments', ['commentTree' => $commentTree, 'page' => $page])
<div class="clearfix"></div>
</div>
@endif
@stop

@section('left')

@if($page->tags->count() > 0)
<section>
@include('entities.tag-list', ['entity' => $page])
</section>
@endif

@if ($page->attachments->count() > 0)
<div id="page-attachments" class="mb-l">
<h5>{{ trans('entities.pages_attachments') }}</h5>
<div class="body">
@include('attachments.list', ['attachments' => $page->attachments])
</div>
</div>
@endif

@if (isset($pageNav) && count($pageNav))
<nav id="page-navigation" class="mb-xl" aria-label="{{ trans('entities.pages_navigation') }}">
<h5>{{ trans('entities.pages_navigation') }}</h5>
<div class="body">
<div class="sidebar-page-nav menu">
@foreach($pageNav as $navItem)
<li class="page-nav-item h{{ $navItem['level'] }}">
<a href="{{ $navItem['link'] }}" class="text-limit-lines-1 block">{{ $navItem['text'] }}</a>
<div class="link-background sidebar-page-nav-bullet"></div>
</li>
@endforeach
</div>
</div>
</nav>
@endif

@include('entities.book-tree', ['book' => $book, 'sidebarTree' => $sidebarTree])
@stop

@section('right')
<div id="page-details" class="entity-details mb-xl">
<h5>{{ trans('common.details') }}</h5>
<div class="blended-links">
@include('entities.meta', ['entity' => $page, 'watchOptions' => $watchOptions])

@if($book->hasPermissions())
<div class="active-restriction">
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $book))
<a href="{{ $book->getUrl('/permissions') }}" class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.books_permissions_active') }}</div>
</a>
@else
<div class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.books_permissions_active') }}</div>
</div>
@endif
</div>
@endif

@if($page->chapter && $page->chapter->hasPermissions())
<div class="active-restriction">
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page->chapter))
<a href="{{ $page->chapter->getUrl('/permissions') }}" class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.chapters_permissions_active') }}</div>
</a>
@else
<div class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.chapters_permissions_active') }}</div>
</div>
@endif
</div>
@endif

@if($page->hasPermissions())
<div class="active-restriction">
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page))
<a href="{{ $page->getUrl('/permissions') }}" class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.pages_permissions_active') }}</div>
</a>
@else
<div class="entity-meta-item">
@icon('lock')
<div>{{ trans('entities.pages_permissions_active') }}</div>
</div>
@endif
</div>
@endif

@if($page->template)
<div class="entity-meta-item">
@icon('template')
<div>{{ trans('entities.pages_is_template') }}</div>
</div>
@endif
</div>
</div>

<div class="actions mb-xl">
<h5>{{ trans('common.actions') }}</h5>

<div class="icon-list text-link">

{{--User Actions--}}
@if(userCan(\BookStack\Permissions\Permission::PageUpdate, $page))
<a href="{{ $page->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</a>
@endif
@if(userCan(\BookStack\Permissions\Permission::PageCreateAll) || userCan(\BookStack\Permissions\Permission::PageCreateOwn) || userCanOnAny(\BookStack\Permissions\Permission::Create, \BookStack\Entities\Models\Book::class) || userCanOnAny(\BookStack\Permissions\Permission::Create, \BookStack\Entities\Models\Chapter::class))
<a href="{{ $page->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span>
</a>
@endif
@if(userCan(\BookStack\Permissions\Permission::PageUpdate, $page))
@if(userCan(\BookStack\Permissions\Permission::PageDelete, $page))
<a href="{{ $page->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
<span>@icon('folder')</span>
<span>{{ trans('common.move') }}</span>
</a>
@endif
@endif
<a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
<span>@icon('history')</span>
<span>{{ trans('entities.revisions') }}</span>
</a>
@if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page))
<a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span>
</a>
@endif
@if(userCan(\BookStack\Permissions\Permission::PageDelete, $page))
<a href="{{ $page->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span>
</a>
@endif

<hr class="primary-background"/>

@if($watchOptions->canWatch() && !$watchOptions->isWatching())
@include('entities.watch-action', ['entity' => $page])
@endif
@if(!user()->isGuest())
@include('entities.favourite-action', ['entity' => $page])
@endif
@if(userCan(\BookStack\Permissions\Permission::ContentExport))
@include('entities.export-menu', ['entity' => $page])
@endif
</div>

</div>
@stop