81d065b9bb
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Successful in 9s
Generic: Project CI / Lint & Validate (pull_request) Successful in 32s
Generic: Standards Compliance / Secret Scanning (push) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 51s
Generic: Standards Compliance / License Header Validation (push) Successful in 9s
Generic: Standards Compliance / Repository Structure Validation (push) Successful in 8s
Generic: Standards Compliance / Coding Standards Check (push) Successful in 10s
Generic: Standards Compliance / Workflow Configuration Check (push) Failing after 9s
Generic: Standards Compliance / Documentation Quality Check (push) Successful in 8s
Generic: Standards Compliance / README Completeness Check (push) Failing after 7s
Generic: Standards Compliance / Version Consistency Check (push) Successful in 1m2s
Generic: Standards Compliance / Script Integrity Validation (push) Successful in 26s
Generic: Standards Compliance / Git Repository Hygiene (push) Successful in 1m5s
Generic: Standards Compliance / File Naming Standards (push) Successful in 10s
Generic: Standards Compliance / Line Length Check (push) Successful in 23s
Generic: Standards Compliance / Insecure Code Pattern Detection (push) Successful in 10s
Generic: Standards Compliance / Code Complexity Analysis (push) Successful in 1m2s
Generic: Standards Compliance / Code Duplication Detection (push) Successful in 1m8s
Generic: Standards Compliance / Dead Code Detection (push) Successful in 16s
Generic: Standards Compliance / File Size Limits (push) Successful in 16s
Generic: Standards Compliance / TODO/FIXME Tracking (push) Successful in 14s
Generic: Standards Compliance / Dependency Vulnerability Scanning (push) Successful in 1m3s
Deploy (Prod) / Build & Deploy to Prod (push) Successful in 5m9s
Generic: Standards Compliance / Broken Link Detection (push) Successful in 12s
Generic: Standards Compliance / API Documentation Coverage (push) Successful in 7s
Generic: Standards Compliance / Accessibility Check (push) Successful in 7s
Generic: Standards Compliance / Performance Metrics (push) Successful in 8s
Generic: Standards Compliance / Binary File Detection (push) Successful in 2m12s
Generic: Standards Compliance / Unused Dependencies Check (push) Successful in 55s
Generic: Standards Compliance / Terraform Configuration Validation (push) Successful in 12s
Generic: Standards Compliance / Enterprise Readiness Check (push) Successful in 55s
Generic: Standards Compliance / Repository Health Check (push) Successful in 54s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Standards Compliance / Compliance Summary (push) Has been cancelled
Comprehensive MokoGIT theme/branding built from the authoritative mokoconsulting.tech
design tokens (MokoOnyx *.custom.css):
- Theme: replace gitea themes entirely with theme-mokogit-{light,dark,auto} (default
mokogit-auto); brand tokens (primary #010156 / accent #3f8ff0, navy nav+footer #112855,
semantic palette); mokogit-brand.css component reskin; runtime custom-stylesheet override
hook (templates/custom/header.tmpl -> mokogit-custom.css, re-skin live with no rebuild).
- Font Awesome 7 Free 7.1.0: vendored unmodified (CSS headers intact + webfonts + verbatim
LICENSE.txt), license-compliant for distribution/white-label (Icons CC BY 4.0, Fonts SIL
OFL 1.1, Code MIT); attributed in THIRD-PARTY-NOTICES.md, README, and wiki.
- Light/dark toggle: navbar sun/moon control, server-persisted via the existing
/user/settings/appearance/theme endpoint (anonymous falls back to localStorage).
- Accessibility menu: 6 options (text resize, color invert, high contrast, link highlight,
readable font, pause animations); localStorage-persisted, applied on <html>, ARIA/keyboard.
- PWA: service worker (network-first navigation + cached offline fallback) + branded offline
page + enhanced manifest (display standalone, navy theme_color, app shortcuts); manifest
icon sourced from the site-admin branding icon if set, else the bundled MokoGIT logo.
Verified: `go build ./...` and `vite build` both green; only mokogit themes emit.
209 lines
8.8 KiB
Go
209 lines
8.8 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package misc
|
|
|
|
import (
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/git"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/httpcache"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/httplib"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/json"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/log"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/setting"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/util"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/modules/web/middleware"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGIT/services/context"
|
|
)
|
|
|
|
func SiteManifest(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "application/manifest+json")
|
|
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
|
|
return
|
|
}
|
|
if req.Method == http.MethodHead {
|
|
return
|
|
}
|
|
|
|
ctx := req.Context()
|
|
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
|
|
appURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/")
|
|
// Icons resolve to admin-uploaded branding (nav-icon writes logo.png, favicon
|
|
// upload writes favicon.*) when set, otherwise to the bundled MokoGIT branded
|
|
// defaults at the same /assets/img/ paths. See routers/web/admin/branding.go.
|
|
manifest := map[string]any{
|
|
"name": setting.AppName,
|
|
"short_name": setting.AppName,
|
|
"description": setting.AppName + " — self-hosted Git service",
|
|
"id": appURL + "/",
|
|
"start_url": appURL + "/",
|
|
"scope": appURL + "/",
|
|
"display": "standalone",
|
|
"display_override": []string{"standalone", "minimal-ui", "browser"},
|
|
"orientation": "any",
|
|
"theme_color": "#112855", // MokoGIT signature navy
|
|
"background_color": "#0e1318",
|
|
"categories": []string{"developer", "productivity"},
|
|
"icons": []map[string]string{
|
|
{"src": absoluteAssetURL + "/assets/img/favicon.svg", "type": "image/svg+xml", "sizes": "any", "purpose": "any"},
|
|
{"src": absoluteAssetURL + "/assets/img/favicon.png", "type": "image/png", "sizes": "256x256", "purpose": "any"},
|
|
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
|
|
{"src": absoluteAssetURL + "/assets/img/logo-small.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable"},
|
|
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512", "purpose": "any"},
|
|
},
|
|
"shortcuts": []map[string]any{
|
|
{"name": "Dashboard", "url": appURL + "/", "icons": []map[string]string{{"src": absoluteAssetURL + "/assets/img/logo-small.png", "sizes": "512x512"}}},
|
|
{"name": "Issues", "url": appURL + "/issues"},
|
|
{"name": "Pull Requests", "url": appURL + "/pulls"},
|
|
{"name": "Explore", "url": appURL + "/explore/repos"},
|
|
},
|
|
}
|
|
_ = json.NewEncoder(w).Encode(manifest)
|
|
}
|
|
|
|
// serviceWorkerJS is the MokoGIT PWA service worker. Tokens are replaced per
|
|
// request so the scope and offline/asset URLs honor AppSubURL. Strategy:
|
|
// - navigation requests: network-first, fall back to the cached offline page
|
|
// - same-origin static assets under the asset prefix: cache-first
|
|
// - everything else: passthrough
|
|
const serviceWorkerJS = `
|
|
const CACHE = '__CACHE__';
|
|
const OFFLINE_URL = '__OFFLINE__';
|
|
const ASSET_PREFIX = '__ASSETS__';
|
|
self.addEventListener('install', (e) => {
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.add(new Request(OFFLINE_URL, {cache: 'reload'}))).then(() => self.skipWaiting()));
|
|
});
|
|
self.addEventListener('activate', (e) => {
|
|
e.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))).then(() => self.clients.claim()));
|
|
});
|
|
self.addEventListener('fetch', (e) => {
|
|
const req = e.request;
|
|
if (req.method !== 'GET') return;
|
|
const url = new URL(req.url);
|
|
if (url.origin !== self.location.origin) return;
|
|
if (req.mode === 'navigate') {
|
|
e.respondWith(fetch(req).catch(() => caches.match(OFFLINE_URL)));
|
|
return;
|
|
}
|
|
if (ASSET_PREFIX && url.pathname.startsWith(ASSET_PREFIX)) {
|
|
e.respondWith(caches.match(req).then((cached) => cached || fetch(req).then((resp) => {
|
|
if (resp && resp.ok && resp.type === 'basic') { const clone = resp.clone(); caches.open(CACHE).then((c) => c.put(req, clone)); }
|
|
return resp;
|
|
}).catch(() => cached)));
|
|
}
|
|
});
|
|
`
|
|
|
|
// ServiceWorker serves the PWA service worker script with a root-scoped
|
|
// Service-Worker-Allowed header so it can control navigations under AppSubURL.
|
|
func ServiceWorker(w http.ResponseWriter, req *http.Request) {
|
|
scope := setting.AppSubURL + "/"
|
|
assetPrefix := strings.TrimSuffix(setting.StaticURLPrefix, "/") + "/assets/"
|
|
js := serviceWorkerJS
|
|
js = strings.ReplaceAll(js, "__CACHE__", "mokogit-pwa-"+setting.AppStartTime.Format("20060102150405"))
|
|
js = strings.ReplaceAll(js, "__OFFLINE__", setting.AppSubURL+"/-/offline")
|
|
js = strings.ReplaceAll(js, "__ASSETS__", assetPrefix)
|
|
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
|
w.Header().Set("Service-Worker-Allowed", scope)
|
|
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
|
|
return
|
|
}
|
|
if req.Method == http.MethodHead {
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(js))
|
|
}
|
|
|
|
// Offline serves a self-contained, branded offline fallback page. It is
|
|
// precached by the service worker and shown when a navigation fails offline.
|
|
func Offline(w http.ResponseWriter, req *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if req.Method == http.MethodHead {
|
|
return
|
|
}
|
|
page := strings.ReplaceAll(offlineHTML, "__NAME__", setting.AppName)
|
|
page = strings.ReplaceAll(page, "__ICON__", strings.TrimSuffix(setting.StaticURLPrefix, "/")+"/assets/img/logo.png")
|
|
_, _ = w.Write([]byte(page))
|
|
}
|
|
|
|
// offlineHTML is a dependency-light offline page (inline styles, MokoGIT navy).
|
|
const offlineHTML = `<!DOCTYPE html>
|
|
<html lang="en" data-theme="mokogit-auto">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Offline · __NAME__</title>
|
|
<style>
|
|
:root { color-scheme: light dark; }
|
|
html,body { height:100%; margin:0; }
|
|
body { display:flex; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif; background:#0e1318; color:#e6ebf1; text-align:center; padding:2rem; }
|
|
.card { max-width:28rem; }
|
|
img { width:72px; height:72px; object-fit:contain; margin-bottom:1.25rem; }
|
|
h1 { font-size:1.5rem; margin:0 0 .5rem; color:#f1f5f9; }
|
|
p { color:#c7ccd4; line-height:1.5; margin:0 0 1.5rem; }
|
|
button { background:#010156; color:#fff; border:0; border-radius:.25rem; padding:.6rem 1.25rem; font-size:1rem; cursor:pointer; }
|
|
button:hover { background:#010149; }
|
|
.bar { height:4px; background:#112855; position:fixed; top:0; left:0; right:0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="bar"></div>
|
|
<div class="card">
|
|
<img src="__ICON__" alt="" onerror="this.style.display='none'">
|
|
<h1>You're offline</h1>
|
|
<p>__NAME__ can't be reached right now. Check your connection and try again — recently visited pages may still be available.</p>
|
|
<button onclick="location.reload()">Retry</button>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
|
|
if !git.DefaultFeatures().SupportProcReceive {
|
|
rw.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
rw.Header().Set("content-type", "text/json;charset=UTF-8")
|
|
_, err := rw.Write([]byte(`{"type":"agit","version":1}`))
|
|
if err != nil {
|
|
log.Error("fail to write result: err: %v", err)
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func DummyOK(w http.ResponseWriter, req *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func RobotsTxt(w http.ResponseWriter, req *http.Request) {
|
|
robotsTxt := util.FilePathJoinAbs(setting.CustomPath, "public/robots.txt")
|
|
if ok, _ := util.IsExist(robotsTxt); !ok {
|
|
robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
|
|
}
|
|
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
|
|
http.ServeFile(w, req, robotsTxt)
|
|
}
|
|
|
|
func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently)
|
|
}
|
|
}
|
|
|
|
func LocationRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
http.Redirect(w, req, target, http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func WebBannerDismiss(ctx *context.Context) {
|
|
_, rev, _ := setting.Config().Instance.WebBanner.ValueRevision(ctx)
|
|
middleware.SetSiteCookie(ctx.Resp, middleware.CookieWebBannerDismissed, strconv.Itoa(rev), 48*3600)
|
|
ctx.JSONOK()
|
|
}
|