Skip to content
Draft
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
31 changes: 31 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Deploy Docs

on:
push:
branches:
- main
- master
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'requirements.txt'
workflow_dispatch:

permissions:
contents: write

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.x'
Comment on lines +21 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the workflow with line numbers
if [ -f .github/workflows/docs.yml ]; then
  cat -n .github/workflows/docs.yml | sed -n '1,220p'
else
  echo "Missing .github/workflows/docs.yml"
fi

Repository: InvoicePlane/Wiki

Length of output: 906


Pin the GitHub Actions to commit SHAs. actions/checkout@v4 and actions/setup-python@v5 are mutable tags in a job with contents: write, so upstream retargeting would run privileged code without a repo change.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 21-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 23-23: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs.yml around lines 21 - 25, Pin the workflow actions
used in the docs job to immutable commit SHAs instead of the mutable tags in the
checkout/setup step. Update the references for actions/checkout and
actions/setup-python in the docs workflow so the job identified by its
actions/checkout and actions/setup-python usage runs only trusted, fixed
revisions while keeping the existing behavior unchanged.

Source: Linters/SAST tools


- name: Install dependencies
run: pip install -r requirements.txt

- name: Deploy to GitHub Pages
run: mkdocs gh-deploy --force
Comment on lines +21 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Disable persisted checkout credentials before dependency install
.github/workflows/docs.yml:21-31actions/checkout@v4 leaves the repo token in git config by default, so pip install -r requirements.txt runs with write access available to any compromised install-time code. Set persist-credentials: false and pass auth only to mkdocs gh-deploy, or switch to the Pages artifact flow.

🧰 Tools
🪛 zizmor (1.26.1)

[warning] 21-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 23-23: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/docs.yml around lines 21 - 31, The docs workflow currently
leaves checkout credentials persisted, so dependency installation can run with
unnecessary write access. Update the `actions/checkout@v4` step in the docs
workflow to disable persisted credentials, then make sure `mkdocs gh-deploy`
still has only the auth it needs for deployment. Keep the change scoped to the
`actions/checkout` and `Deploy to GitHub Pages` steps in
`.github/workflows/docs.yml`.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@
_ide_helper.php
.env
.idea
/site
__pycache__
*.pyc
177 changes: 120 additions & 57 deletions app/Http/Controllers/WikiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,109 +2,172 @@

namespace App\Http\Controllers;

use App\WikiPage;
use Illuminate\Support\Facades\Config;

class WikiController extends Controller
{
/**
* @var WikiPage
*/
protected $wikiPage;

public function __construct(WikiPage $wikiPage)
{
$this->wikiPage = $wikiPage;
}

public function getPage($locale, $version = '', $dir = '', $page = '')
{
// Get default vars first
$default_locale = Config::get('app.locale');
$default_locale = Config::get('app.locale');
$default_version = Config::get('app.version');

// Check if the requested page has a redirect
// Check if the requested page has a redirect configured.
$redirects = Config::get('routes.redirects');

if (isset($redirects[$version][$dir][$page])) {
return redirect()->to($redirects[$version][$dir][$page]);
}

// Redirect to the default locale if the requested one is not available
// Redirect to the default locale if the requested one is not available.
$locales = Config::get('app.available_locales');
if (!isset($locales[$locale])) {
if (! isset($locales[$locale])) {
$redirect_url = $default_locale;
if ($version) {
$redirect_url .= '/' . $version;
}
if ($dir) {
$redirect_url .= '/' . $dir;
}
if ($page) {
$redirect_url .= '/' . $page;
}
if ($version) { $redirect_url .= '/' . $version; }
if ($dir) { $redirect_url .= '/' . $dir; }
if ($page) { $redirect_url .= '/' . $page; }
return redirect()->to($redirect_url);
}

// Redirect to the default version if the requested one is not available
// Normalize version to underscore format (1.6 → 1_6).
$versions = Config::get('app.versions');
$version = str_replace('.', '_', $version);
if (empty($version) || !in_array($version, $versions)) {
$version = str_replace('.', '_', $version);

if (empty($version) || ! in_array($version, $versions)) {
$redirect_url = $locale . '/' . str_replace('_', '.', $default_version);
if (!empty($dir)) {
$redirect_url .= '/' . $dir;
if (! empty($dir)) { $redirect_url .= '/' . $dir; }
if (! empty($page)) { $redirect_url .= '/' . $page; }
return redirect()->to($redirect_url);
}

// Build the sub-path used by the topbar version switcher.
if (! empty($dir) && ! empty($page)) {
$current_url = '/' . $dir . '/' . $page;
} elseif (! empty($dir)) {
$current_url = '/' . $dir;
} else {
$current_url = '/';
}

view()->share([
'current_dir' => $dir,
'current_page' => $page,
'current_url' => $current_url,
'current_version' => str_replace('_', '.', $version),
]);

if ($this->usesMarkdown($version)) {
return $this->serveMarkdown($locale, $version, $dir, $page, $current_url, $versions, $default_version);
}

return $this->serveBlade($locale, $version, $dir, $page, $current_url, $versions, $default_version);
}

/**
* Serve a page from a Markdown file.
* Used for versions >= 1.7.
* Falls back only within the markdown version pool.
*/
protected function serveMarkdown($locale, $version, $dir, $page, $current_url, $versions, $default_version)
{
$markdown_versions = array_values(array_filter($versions, function ($v) {
return $this->usesMarkdown($v);
}));

// Try requested version first, then older markdown versions newest-first.
$try_order = array_merge(
[$version],
array_values(array_diff(array_reverse($markdown_versions), [$version]))
);

foreach ($try_order as $try_version) {
if (! $this->wikiPage->exists($locale, $try_version, $dir, $page)) {
continue;
}
if (!empty($page)) {
$redirect_url .= '/' . $page;

// Found in an older version — redirect rather than serve cross-version.
if ($try_version !== $version) {
return redirect()->to(
'/' . $locale . '/' . str_replace('_', '.', $try_version) . $current_url
);
}
return redirect()->to($redirect_url);

$sidebar_view = $locale . '.' . $version . '.sidebar';

return view('wiki.page', [
'content' => $this->wikiPage->get($locale, $version, $dir, $page),
'page_title' => $this->wikiPage->title($locale, $version, $dir, $page),
'sidebar_content' => view()->exists($sidebar_view) ? view($sidebar_view) : null,
]);
}

// Check if the requested page exists, if not redirect to home
return redirect()->to('/' . $locale . '/' . str_replace('_', '.', $default_version));
}

/**
* Serve a page from a Blade template.
* Used for versions <= 1.6.
* Falls back only within the blade version pool.
*/
protected function serveBlade($locale, $version, $dir, $page, $current_url, $versions, $default_version)
{
// Build the dot-notation view name from URL segments.
if (empty($dir) && empty($page)) {
$requested_view = '.root';
$current_url = '/';
} elseif (empty($page)) {
$requested_view = '.' . str_replace('-', '_', $dir);
$current_url = '/' . $dir;
} else {
$requested_view = '.' . str_replace('-', '_', $dir) . '.' . str_replace('-', '_', $page);
$current_url = '/' . $dir . '/' . $page;
}

$requested_page = $locale . '.' . $version . $requested_view;

// Check if the view exists
if (!view()->exists($requested_page)) {

// Check if the page exists for an older version
$reverse_versions = array_reverse($versions);
if (! view()->exists($requested_page)) {
// Fall back through older blade-only versions (newest first).
$blade_versions = array_values(array_filter($versions, function ($v) {
return ! $this->usesMarkdown($v);
}));

foreach($reverse_versions as $old_version) {
$requested_page = $locale . '.' . $old_version . $requested_view;
foreach (array_reverse($blade_versions) as $old_version) {
$candidate = $locale . '.' . $old_version . $requested_view;

if (view()->exists($requested_page)) {
// View found, redirect to the page
return redirect()->to('/' . $locale . '/' . str_replace('_', '.', $old_version) . $current_url);
if (view()->exists($candidate)) {
return redirect()->to(
'/' . $locale . '/' . str_replace('_', '.', $old_version) . $current_url
);
}

$requested_page = '';
continue;
}

return redirect()->to('/' . $locale . '/' . str_replace('_', '.', $default_version));
}

// Load the page if found
if ($requested_page) {
$sidebar_content_view = $locale . '.' . $version . '.sidebar';

view()->share([
'current_dir' => $dir,
'current_page' => $page,
'current_url' => $current_url,
'current_version' => str_replace('_', '.', $version),
]);
$sidebar_view = $locale . '.' . $version . '.sidebar';

return view($requested_page)->with([
'sidebar_content' => view($sidebar_content_view),
]);
}
return view($requested_page)->with([
'sidebar_content' => view($sidebar_view),
]);
}

// Load the default page
return redirect()->to('/' . $locale . '/' . str_replace('_', '.', $default_version));
/**
* Versions >= 1.7 are served from Markdown files in docs/.
* Versions <= 1.6 are served from Blade templates in wiki/.
*/
protected function usesMarkdown(string $version): bool
{
return version_compare(str_replace('_', '.', $version), '1.7', '>=');
}

public function getTestPage()
{
return "TEST";
return 'TEST';
}
}
24 changes: 24 additions & 0 deletions app/Markdown/GithubFlavoredMarkdownConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Markdown;

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdown\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;

class GithubFlavoredMarkdownConverter
{
public function convert(string $markdown): string
{
$environment = new Environment([
'html_input' => 'allow',
'allow_unsafe_links' => false,
]);

$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());

return (string) (new MarkdownConverter($environment))->convert($markdown);
}
}
Loading