mirror of
https://codeberg.org/readeck/readeck.git
synced 2025-12-22 05:07:08 +00:00
This is a big change, as it renames locale and documentation folders. - renamed translation folders following BCP47 codes - improved display of available translations Each translation is now displayed in its own language and in the current user's language. - the default language is now "en" instead of "en-US" - renamed folders in docs/src and docs/translations - don't copy images from docs/src when they don't contain an index.md file - added a user.Lang() method that returns a code that is guaranteed to be available in the locale list
320 lines
8.2 KiB
Go
320 lines
8.2 KiB
Go
// SPDX-FileCopyrightText: © 2023 Olivier Meunier <olivier@neokraft.net>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package docs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/komkom/toml"
|
|
|
|
"codeberg.org/readeck/readeck/configs"
|
|
"codeberg.org/readeck/readeck/internal/auth"
|
|
"codeberg.org/readeck/readeck/internal/bookmarks"
|
|
"codeberg.org/readeck/readeck/internal/db"
|
|
"codeberg.org/readeck/readeck/internal/server"
|
|
"codeberg.org/readeck/readeck/internal/server/urls"
|
|
"codeberg.org/readeck/readeck/locales"
|
|
"codeberg.org/readeck/readeck/pkg/http/csp"
|
|
)
|
|
|
|
type (
|
|
ctxFileKey struct{}
|
|
ctxSectionKey struct{}
|
|
ctxLanguageKey struct{}
|
|
)
|
|
|
|
type helpHandlers struct {
|
|
chi.Router
|
|
srv *server.Server
|
|
}
|
|
|
|
type licenseInfo struct {
|
|
Name string
|
|
License string
|
|
Author string
|
|
URL string
|
|
Copyright string
|
|
}
|
|
|
|
const routePrefix = "/docs"
|
|
|
|
// SetupRoutes mounts the routes for the auth domain.
|
|
func SetupRoutes(s *server.Server) {
|
|
handler := &helpHandlers{
|
|
chi.NewRouter(),
|
|
s,
|
|
}
|
|
|
|
// File routes
|
|
for _, f := range manifest.Files {
|
|
if f.IsDocument {
|
|
continue
|
|
}
|
|
handler.With(handler.withFile(f)).Get("/"+f.Route, handler.serveStatic)
|
|
}
|
|
|
|
// Document routes
|
|
// docHandler serves the document and requires authentication
|
|
docHandler := handler.With(server.AuthenticatedRouter(server.WithRedirectLogin).Middlewares()...)
|
|
for tag, section := range manifest.Sections {
|
|
for _, f := range section.Files {
|
|
// Document
|
|
docHandler.With(
|
|
server.WithPermission("docs", "read"),
|
|
handler.withFile(f),
|
|
handler.withSection(tag, section),
|
|
).Get("/"+f.Route, handler.serveDocument)
|
|
|
|
// Aliases
|
|
for _, alias := range f.Aliases {
|
|
docHandler.With(
|
|
server.WithPermission("docs", "read"),
|
|
).Get("/"+alias, handler.serveRedirect(routePrefix+"/"+f.Route))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Changelog route
|
|
f := manifest.Files["changelog"]
|
|
docHandler.With(
|
|
server.WithPermission("system", "read"),
|
|
handler.withFile(f),
|
|
).Get("/changelog", handler.serveDocument)
|
|
|
|
// About page
|
|
docHandler.With(
|
|
server.WithPermission("system", "read"),
|
|
).Get("/about", handler.serveAbout)
|
|
|
|
// Documentation
|
|
docHandler.With(server.WithPermission("docs", "read")).Get("/", handler.localeRedirect)
|
|
docHandler.With(server.WithPermission("docs", "read")).Get("/{path}", handler.localeRedirect)
|
|
|
|
// API documentation
|
|
docHandler.With(
|
|
server.WithPermission("docs", "read"),
|
|
).Group(func(r chi.Router) {
|
|
r.Get("/api", handler.serveAPIDocs)
|
|
r.Get("/api.json", handler.serveAPISchema)
|
|
})
|
|
|
|
s.AddRoute(routePrefix, handler)
|
|
}
|
|
|
|
func (h *helpHandlers) withFile(f *File) func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if f == nil {
|
|
server.Status(w, r, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), ctxFileKey{}, f)
|
|
|
|
server.WriteEtag(w, r, f)
|
|
server.WithCaching(next).ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *helpHandlers) withSection(tag string, section *Section) func(next http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := context.WithValue(r.Context(), ctxSectionKey{}, section)
|
|
ctx = context.WithValue(ctx, ctxLanguageKey{}, tag)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *helpHandlers) getSection(r *http.Request) (*Section, string) {
|
|
if section, ok := r.Context().Value(ctxSectionKey{}).(*Section); ok {
|
|
return section, r.Context().Value(ctxLanguageKey{}).(string)
|
|
}
|
|
|
|
tag := server.Locale(r).Tag.String()
|
|
if _, ok := manifest.Sections[tag]; !ok {
|
|
tag = "en"
|
|
}
|
|
return manifest.Sections[tag], tag
|
|
}
|
|
|
|
func (h *helpHandlers) serveDocument(w http.ResponseWriter, r *http.Request) {
|
|
f, _ := r.Context().Value(ctxFileKey{}).(*File)
|
|
|
|
fd, err := Files.Open(f.File)
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
defer fd.Close()
|
|
|
|
var contents strings.Builder
|
|
io.Copy(&contents, fd)
|
|
repl := strings.NewReplacer(
|
|
"readeck-instance://", urls.AbsoluteURL(r, "/").String(),
|
|
)
|
|
buf := new(bytes.Buffer)
|
|
repl.WriteString(buf, contents.String())
|
|
|
|
section, tag := h.getSection(r)
|
|
tr := locales.LoadTranslation(tag)
|
|
ctx := server.TC{
|
|
"TOC": section.TOC,
|
|
"Language": tag,
|
|
"Title": f.Title,
|
|
"HTML": buf,
|
|
}
|
|
ctx.SetBreadcrumbs([][2]string{
|
|
{tr.Gettext("Documentation"), urls.AbsoluteURL(r, "/docs", tag, "/").String()},
|
|
{f.Title},
|
|
})
|
|
|
|
server.RenderTemplate(w, r, http.StatusOK, "docs/index", ctx)
|
|
}
|
|
|
|
func (h *helpHandlers) serveStatic(w http.ResponseWriter, r *http.Request) {
|
|
f, _ := r.Context().Value(ctxFileKey{}).(*File)
|
|
fd, err := Files.Open(f.File)
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
defer fd.Close()
|
|
|
|
http.ServeContent(w, r, f.File, time.Time{}, fd)
|
|
}
|
|
|
|
func (h *helpHandlers) localeRedirect(w http.ResponseWriter, r *http.Request) {
|
|
tag := server.Locale(r).Tag.String()
|
|
if _, ok := manifest.Sections[tag]; !ok {
|
|
tag = "en"
|
|
}
|
|
|
|
server.Redirect(w, r, routePrefix+"/"+tag+"/"+chi.URLParam(r, "path"))
|
|
}
|
|
|
|
func (h *helpHandlers) serveRedirect(to string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
server.Redirect(w, r, to)
|
|
}
|
|
}
|
|
|
|
func (h *helpHandlers) serveAbout(w http.ResponseWriter, r *http.Request) {
|
|
fp, err := assets.Open("licenses/licenses.toml")
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
licenses := map[string][]licenseInfo{}
|
|
dec := json.NewDecoder(toml.New(fp))
|
|
if err = dec.Decode(&licenses); err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
slices.SortFunc(licenses["licenses"], func(a, b licenseInfo) int {
|
|
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
|
|
})
|
|
|
|
dbUsageVal, err := db.Driver().DiskUsage()
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
diskUsageVal, err := bookmarks.Bookmarks.DiskUsage()
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
buildInfo := new(strings.Builder)
|
|
if info, ok := debug.ReadBuildInfo(); ok {
|
|
for _, x := range info.Settings {
|
|
fmt.Fprintf(buildInfo, "%s: %s\n", x.Key, strings.ReplaceAll(x.Value, ",", ", "))
|
|
}
|
|
}
|
|
|
|
section, tag := h.getSection(r)
|
|
tr := locales.LoadTranslation(tag)
|
|
ctx := server.TC{
|
|
"TOC": section.TOC,
|
|
"Language": tag,
|
|
"Version": configs.Version(),
|
|
"BuildTime": configs.BuildTime(),
|
|
"BuildInfo": buildInfo,
|
|
"Licenses": licenses["licenses"],
|
|
"OS": runtime.GOOS,
|
|
"Arch": runtime.GOARCH,
|
|
"GoVersion": runtime.Version(),
|
|
"DBConnecter": db.Driver().Name(),
|
|
"DBVersion": db.Driver().Version(),
|
|
"DBSize": dbUsageVal,
|
|
"DiskUsage": diskUsageVal,
|
|
}
|
|
ctx.SetBreadcrumbs([][2]string{
|
|
{tr.Gettext("Documentation"), urls.AbsoluteURL(r, "/docs", tag, "/").String()},
|
|
{tr.Gettext("About Readeck")},
|
|
})
|
|
|
|
server.RenderTemplate(w, r, http.StatusOK, "docs/about", ctx)
|
|
}
|
|
|
|
func (h *helpHandlers) serveAPISchema(w http.ResponseWriter, r *http.Request) {
|
|
fd, err := Files.Open("api.json")
|
|
if err != nil {
|
|
server.Err(w, r, err)
|
|
return
|
|
}
|
|
defer fd.Close()
|
|
|
|
var contents strings.Builder
|
|
io.Copy(&contents, fd)
|
|
repl := strings.NewReplacer(
|
|
"__ROOT_URI__", strings.TrimSuffix(urls.AbsoluteURL(r, "/").String(), "/"),
|
|
"__BASE_URI__", urls.AbsoluteURL(r, "/api").String(),
|
|
)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
repl.WriteString(w, contents.String())
|
|
}
|
|
|
|
func (h *helpHandlers) serveAPIDocs(w http.ResponseWriter, r *http.Request) {
|
|
// By including a web component full of inline styles, we need
|
|
// to relax the style-src policy.
|
|
policy := server.GetCSPHeader(r).Clone()
|
|
policy.Set("style-src", csp.ReportSample, csp.Self, csp.UnsafeInline)
|
|
policy.Write(w.Header())
|
|
|
|
tr := server.Locale(r)
|
|
tc := server.TC{
|
|
"Schema": urls.AbsoluteURL(r, "/docs/api.json"),
|
|
}
|
|
tc.SetBreadcrumbs([][2]string{
|
|
{tr.Gettext("Documentation"), urls.AbsoluteURL(r, "/docs", tr.Tag.String(), "/").String()},
|
|
{"API"},
|
|
})
|
|
|
|
hideBadges := []string{}
|
|
if !auth.GetRequestUser(r).HasPermission("api:cookbook", "read") {
|
|
hideBadges = append(hideBadges, "admin only")
|
|
}
|
|
tc["HideBadges"] = strings.Join(hideBadges, ",")
|
|
|
|
server.RenderTemplate(w, r, http.StatusOK, "docs/api-docs", tc)
|
|
}
|