diff --git a/config/config.go b/config/config.go index 4c380c3..ce8664a 100644 --- a/config/config.go +++ b/config/config.go @@ -3,8 +3,6 @@ package config import ( "context" - "errors" - "fmt" "log" "net/url" "strings" @@ -15,6 +13,7 @@ import ( // "github.com/goware/urlx" "github.com/sethvargo/go-envconfig" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/token_manager" ) @@ -102,10 +101,10 @@ func validateURL(urlString string, urlType string) (*url.URL, error) { } // Ensure both scheme and host are present in the URL if parsedURL.Scheme == "" || parsedURL.Host == "" { - return nil, fmt.Errorf("%s URL is invalid: %s. Please specify a complete URL with scheme and host, e.g. https://example.com", urlType, urlString) + return nil, i18n.Errorf("%s URL is invalid: %s. Please specify a complete URL with scheme and host, e.g. https://example.com", urlType, urlString) } if strings.HasSuffix(parsedURL.Path, "/") { - return nil, fmt.Errorf("%s URL path (%s) cannot end in /: %s. PixivFE does not support this now", urlType, parsedURL.Path, urlString) + return nil, i18n.Errorf("%s URL path (%s) cannot end in /: %s. PixivFE does not support this now", urlType, parsedURL.Path, urlString) } return parsedURL, nil } @@ -152,13 +151,13 @@ func (s *ServerConfig) LoadConfig() error { if s.Port == "" && s.UnixSocket == "" { log.Fatalln("Either PIXIVFE_PORT or PIXIVFE_UNIXSOCKET has to be set.") - return errors.New("Either PIXIVFE_PORT or PIXIVFE_UNIXSOCKET has to be set") + return i18n.Error("Either PIXIVFE_PORT or PIXIVFE_UNIXSOCKET has to be set") } // a check for tokens if len(s.Token) < 1 { log.Fatalln("PIXIVFE_TOKEN has to be set. Visit https://pixivfe-docs.pages.dev/hosting/hosting-pixivfe for more details.") - return errors.New("PIXIVFE_TOKEN has to be set. Visit https://pixivfe-docs.pages.dev/hosting/hosting-pixivfe for more details") + return i18n.Error("PIXIVFE_TOKEN has to be set. Visit https://pixivfe-docs.pages.dev/hosting/hosting-pixivfe for more details") } // Validate proxy server URL diff --git a/core/discovery.go b/core/discovery.go index 7cb2492..4cea802 100644 --- a/core/discovery.go +++ b/core/discovery.go @@ -1,12 +1,13 @@ package core import ( - "fmt" + "net/http" - "codeberg.org/vnpower/pixivfe/v2/server/session" "github.com/goccy/go-json" "github.com/tidwall/gjson" - "net/http" + + "codeberg.org/vnpower/pixivfe/v2/i18n" + "codeberg.org/vnpower/pixivfe/v2/server/session" ) func GetDiscoveryArtwork(r *http.Request, mode string) ([]ArtworkBrief, error) { @@ -22,7 +23,7 @@ func GetDiscoveryArtwork(r *http.Request, mode string) ([]ArtworkBrief, error) { } resp = session.ProxyImageUrl(r, resp) if !gjson.Valid(resp) { - return nil, fmt.Errorf("Invalid JSON: %v", resp) + return nil, i18n.Errorf("Invalid JSON: %v", resp) } data := gjson.Get(resp, "thumbnails.illust").String() @@ -47,7 +48,7 @@ func GetDiscoveryNovels(r *http.Request, mode string) ([]NovelBrief, error) { } resp = session.ProxyImageUrl(r, resp) if !gjson.Valid(resp) { - return nil, fmt.Errorf("Invalid JSON: %v", resp) + return nil, i18n.Errorf("Invalid JSON: %v", resp) } data := gjson.Get(resp, "thumbnails.novel").String() diff --git a/core/index.go b/core/index.go index 48a0e9b..db96586 100644 --- a/core/index.go +++ b/core/index.go @@ -2,11 +2,13 @@ package core import ( "fmt" + "net/http" - "codeberg.org/vnpower/pixivfe/v2/server/session" "github.com/goccy/go-json" "github.com/tidwall/gjson" - "net/http" + + "codeberg.org/vnpower/pixivfe/v2/i18n" + "codeberg.org/vnpower/pixivfe/v2/server/session" ) type Pixivision struct { @@ -59,7 +61,7 @@ func GetLanding(r *http.Request, mode string) (*LandingArtworks, error) { resp = session.ProxyImageUrl(r, resp) if !gjson.Valid(resp) { - return nil, fmt.Errorf("Invalid JSON: %v", resp) + return nil, i18n.Errorf("Invalid JSON: %v", resp) } artworks := map[string]ArtworkBrief{} diff --git a/core/pixivision.go b/core/pixivision.go index f6ab2df..4cf4123 100644 --- a/core/pixivision.go +++ b/core/pixivision.go @@ -1,7 +1,6 @@ package core import ( - "errors" "fmt" "html" "io" @@ -10,6 +9,8 @@ import ( "time" "github.com/PuerkitoBio/goquery" + + "codeberg.org/vnpower/pixivfe/v2/i18n" ) const PixivDatetimeLayout = "2006-01-02" @@ -37,7 +38,7 @@ func executeRequest(req *http.Request) (*http.Response, error) { client := http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) if resp.StatusCode != 200 { - return nil, errors.New("Pixivision: Page not found") + return nil, i18n.Error("Pixivision: Page not found") } if err != nil { return nil, err @@ -126,7 +127,7 @@ func PixivisionGetHomepage(r *http.Request, page string, lang ...string) ([]Pixi } if resp.StatusCode == 404 { - return articles, errors.New("We couldn't find the page you're looking for") + return articles, i18n.Error("We couldn't find the page you're looking for") } doc, err := goquery.NewDocumentFromReader(resp.Body) diff --git a/core/requests.go b/core/requests.go index bc2e598..5ab1bb6 100644 --- a/core/requests.go +++ b/core/requests.go @@ -4,12 +4,12 @@ import ( "bytes" "context" "errors" - "fmt" "io" "net/http" "time" - config "codeberg.org/vnpower/pixivfe/v2/config" + "codeberg.org/vnpower/pixivfe/v2/config" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/audit" "codeberg.org/vnpower/pixivfe/v2/server/request_context" "codeberg.org/vnpower/pixivfe/v2/server/token_manager" @@ -65,7 +65,7 @@ func retryRequest(ctx context.Context, reqFunc func(context.Context, string) (*r if token == nil && !isPost { tokenManager.ResetAllTokens() - return nil, fmt.Errorf("All tokens (%d) are timed out, resetting all tokens to their initial good state.\n"+ + return nil, i18n.Errorf("All tokens (%d) are timed out, resetting all tokens to their initial good state.\n"+ "Consider providing additional tokens in PIXIVFE_TOKEN or reviewing API request level backoff configuration.\n"+ "Please refer the following documentation for additional information:\n"+ "- https://pixivfe-docs.pages.dev/hosting/obtaining-pixivfe-token/\n"+ @@ -118,7 +118,7 @@ func retryRequest(ctx context.Context, reqFunc func(context.Context, string) (*r }, nil } - lastErr = fmt.Errorf("HTTP status code: %d", resp.StatusCode) + lastErr = i18n.Errorf("HTTP status code: %d", resp.StatusCode) if userToken == "" && !isPost { tokenManager.MarkTokenStatus(token, token_manager.TimedOut) @@ -132,7 +132,7 @@ func retryRequest(ctx context.Context, reqFunc func(context.Context, string) (*r } } - return nil, fmt.Errorf("Max retries reached. Last error: %v", lastErr) + return nil, i18n.Errorf("Max retries reached. Last error: %v", lastErr) } // API_GET performs a GET request to the Pixiv API with automatic retries @@ -161,13 +161,13 @@ func API_GET_UnwrapJson(ctx context.Context, url string, userToken string) (stri } if !gjson.Valid(resp.Body) { - return "", fmt.Errorf("Invalid JSON: %v", resp.Body) + return "", i18n.Errorf("Invalid JSON: %v", resp.Body) } err2 := gjson.Get(resp.Body, "error") if !err2.Exists() { - return "", errors.New("Incompatible request body") + return "", i18n.Error("Incompatible request body") } if err2.Bool() { @@ -180,7 +180,7 @@ func API_GET_UnwrapJson(ctx context.Context, url string, userToken string) (stri // API_POST performs a POST request to the Pixiv API with automatic retries func API_POST(ctx context.Context, url, payload, userToken, csrf string, isJSON bool) error { if userToken == "" { - return errors.New("userToken is required for POST requests") + return i18n.Error("userToken is required for POST requests") } _, err := retryRequest(ctx, func(ctx context.Context, token string) (*retryablehttp.Request, error) { @@ -236,7 +236,7 @@ func proxyWorker(jobs <-chan ProxyJob) { func processProxyJob(job ProxyJob) error { resp, err := utils.HttpClient.Do(job.Request) if err != nil { - return fmt.Errorf("failed to process request: %w", err) + return i18n.Errorf("failed to process request: %w", err) } defer resp.Body.Close() @@ -252,7 +252,7 @@ func processProxyJob(job ProxyJob) error { // Copy the body from the response to the original writer _, err = io.Copy(job.Response, resp.Body) if err != nil { - return fmt.Errorf("failed to copy response body: %w", err) + return i18n.Errorf("failed to copy response body: %w", err) } return nil diff --git a/core/user.go b/core/user.go index 257bd0b..5e4edf7 100644 --- a/core/user.go +++ b/core/user.go @@ -1,14 +1,15 @@ package core import ( - "errors" "fmt" "math" "sort" + "net/http" + + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/session" "github.com/goccy/go-json" - "net/http" ) // pixivfe internal data type. not used by pixiv. @@ -30,7 +31,7 @@ func (s UserArtCategory) Validate() error { s != UserArt_Manga && s != UserArt_Bookmarks && s != UserArt_Novel { - return fmt.Errorf("Invalid work category: %#v. "+`Only "%s", "%s", "%s", "%s", "%s" and "%s" are available`, s, UserArt_Any, UserArt_AnyAlt, UserArt_Illustration, UserArt_Manga, UserArt_Bookmarks, UserArt_Novel) + return i18n.Errorf("Invalid work category: %#v. "+`Only "%s", "%s", "%s", "%s", "%s" and "%s" are available`, s, UserArt_Any, UserArt_AnyAlt, UserArt_Illustration, UserArt_Manga, UserArt_Bookmarks, UserArt_Novel) } else { return nil } @@ -234,7 +235,7 @@ func GetUserArtworksID(r *http.Request, id string, category UserArtCategory, pag worksPerPage := 30.0 if page < 1 || float64(page) > math.Ceil(worksNumber/worksPerPage)+1.0 { - return "", -1, errors.New("No page available.") + return "", -1, i18n.Error("No page available.") } start := (page - 1) * int(worksPerPage) diff --git a/i18n/localize_errors.go b/i18n/localize_errors.go new file mode 100644 index 0000000..932c5d2 --- /dev/null +++ b/i18n/localize_errors.go @@ -0,0 +1,16 @@ +package i18n + +import ( + "errors" + "fmt" +) + +func Error(text string) error { + text = Lookup(text) + return errors.New(text) +} + +func Errorf(format string, a ...any) error { + format = Lookup(format) + return fmt.Errorf(format, a...) +} diff --git a/i18n/lookup.go b/i18n/lookup.go new file mode 100644 index 0000000..e7d4af0 --- /dev/null +++ b/i18n/lookup.go @@ -0,0 +1,6 @@ +package i18n + +func Lookup(text string) string { + // todo + return text +} diff --git a/main.go b/main.go index 39596e6..3a6284a 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "log" "net" "net/http" @@ -14,6 +13,7 @@ import ( "time" "codeberg.org/vnpower/pixivfe/v2/config" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/audit" "codeberg.org/vnpower/pixivfe/v2/server/middleware" "codeberg.org/vnpower/pixivfe/v2/server/proxy_checker" @@ -86,7 +86,7 @@ func run_sass() { runtime.LockOSThread() // Go quirk https://github.com/golang/go/issues/27505 err := cmd.Run() if err != nil { - log.Print(fmt.Errorf("when running sass: %w", err)) + log.Print(i18n.Errorf("when running sass: %w", err), err) } } diff --git a/semgrep.yml b/semgrep.yml index 7b029a9..ac5b7f0 100644 --- a/semgrep.yml +++ b/semgrep.yml @@ -164,15 +164,23 @@ rules: - pattern-not-inside: | ... defer $RESP.Body.Close() -- id: "13" - message: "error string" +- id: "13a" + message: "untranslated errors.New" languages: [go] - severity: INFO - pattern-either: - - pattern: | - errors.New(...) - - pattern: | - fmt.Errorf(...) + severity: ERROR + pattern: | + errors.New("$MSG") + fix: | + i18n.Error("$MSG") +- id: "13b" + message: "untranslated fmt.Errorf" + languages: [go] + severity: ERROR + pattern: | + fmt.Errorf("$MSG", $...ARGS) + fix: | + i18n.Errorf("$MSG", $...ARGS) + - id: "14" message: "non-global regexp.MustCompile" languages: [go] diff --git a/server/audit/audit_init.go b/server/audit/audit_init.go index acd80a3..5b27400 100644 --- a/server/audit/audit_init.go +++ b/server/audit/audit_init.go @@ -1,11 +1,11 @@ package audit import ( - "fmt" "log" "os" "codeberg.org/vnpower/pixivfe/v2/config" + "codeberg.org/vnpower/pixivfe/v2/i18n" ) var optionSaveResponse bool @@ -26,7 +26,7 @@ func Init(saveResponse bool) error { if err := os.MkdirAll(savePath, 0o700); err != nil { log.Printf("Error creating response save directory: %v", err) - return fmt.Errorf("failed to create response save directory: %w", err) + return i18n.Errorf("failed to create response save directory: %w", err) } return nil diff --git a/server/audit/audit_log.go b/server/audit/audit_log.go index 2f252ab..958d5fb 100644 --- a/server/audit/audit_log.go +++ b/server/audit/audit_log.go @@ -3,14 +3,15 @@ package audit import ( - "fmt" "log" "os" "path" "time" - "codeberg.org/vnpower/pixivfe/v2/config" "github.com/oklog/ulid/v2" + + "codeberg.org/vnpower/pixivfe/v2/config" + "codeberg.org/vnpower/pixivfe/v2/i18n" ) // Logger is a custom logger with no timestamp prefix, as we control the timestamps in our log messages. @@ -77,7 +78,7 @@ func writeResponseBodyToFile(body string) (string, error) { // Write the body to the file with read/write permissions for the owner only err := os.WriteFile(filename, []byte(body), 0o600) if err != nil { - return "", fmt.Errorf("failed to write response body to file %s: %w", filename, err) + return "", i18n.Errorf("failed to write response body to file %s: %w", filename, err) } log.Printf("Successfully wrote response body to file: %s", filename) return filename, nil diff --git a/server/middleware/router.go b/server/middleware/router.go index 8f33625..e8ee504 100644 --- a/server/middleware/router.go +++ b/server/middleware/router.go @@ -1,12 +1,13 @@ package middleware import ( - "errors" "net/http" "strings" - "codeberg.org/vnpower/pixivfe/v2/server/routes" "github.com/gorilla/mux" + + "codeberg.org/vnpower/pixivfe/v2/i18n" + "codeberg.org/vnpower/pixivfe/v2/server/routes" ) // handleStripPrefix is a utility function that combines path prefix matching with @@ -129,7 +130,7 @@ func DefineRoutes() *mux.Router { // Fallback route (if nothing else matches) // This ensures that a proper HTTP 404 error is returned for undefined routes router.NewRoute().HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - routes.ErrorPage(w, r, errors.New("Route not found"), http.StatusNotFound) + routes.ErrorPage(w, r, i18n.Error("Route not found"), http.StatusNotFound) }) return router diff --git a/server/routes/actions.go b/server/routes/actions.go index 470911c..30d9392 100644 --- a/server/routes/actions.go +++ b/server/routes/actions.go @@ -1,11 +1,11 @@ package routes import ( - "errors" "fmt" "net/http" "codeberg.org/vnpower/pixivfe/v2/core" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/session" "codeberg.org/vnpower/pixivfe/v2/server/utils" ) @@ -20,7 +20,7 @@ func AddBookmarkRoute(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if id == "" { - return errors.New("No ID provided.") + return i18n.Error("No ID provided.") } URL := "https://www.pixiv.net/ajax/illusts/bookmarks/add" @@ -48,7 +48,7 @@ func DeleteBookmarkRoute(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if id == "" { - return errors.New("No ID provided.") + return i18n.Error("No ID provided.") } // You can't unlike @@ -72,7 +72,7 @@ func LikeRoute(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if id == "" { - return errors.New("No ID provided.") + return i18n.Error("No ID provided.") } URL := "https://www.pixiv.net/ajax/illusts/like" diff --git a/server/routes/artwork.go b/server/routes/artwork.go index 7a76bb8..1aa2924 100644 --- a/server/routes/artwork.go +++ b/server/routes/artwork.go @@ -2,16 +2,17 @@ package routes import ( "fmt" + "net/http" "strconv" "codeberg.org/vnpower/pixivfe/v2/core" - "net/http" + "codeberg.org/vnpower/pixivfe/v2/i18n" ) func ArtworkPage(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if _, err := strconv.Atoi(id); err != nil { - return fmt.Errorf("Invalid ID: %s", id) + return i18n.Errorf("Invalid ID: %s", id) } illust, err := core.GetArtworkByID(r, id, true) diff --git a/server/routes/artworkMulti.go b/server/routes/artworkMulti.go index 91008b3..da5b307 100644 --- a/server/routes/artworkMulti.go +++ b/server/routes/artworkMulti.go @@ -2,12 +2,13 @@ package routes import ( "fmt" + "net/http" "strconv" "strings" "sync" "codeberg.org/vnpower/pixivfe/v2/core" - "net/http" + "codeberg.org/vnpower/pixivfe/v2/i18n" ) func ArtworkMultiPage(w http.ResponseWriter, r *http.Request) error { @@ -26,7 +27,7 @@ func ArtworkMultiPage(w http.ResponseWriter, r *http.Request) error { var err_global error = nil for i, id := range ids { if _, err := strconv.Atoi(id); err != nil { - err_global = fmt.Errorf("Invalid ID: %s", id) + err_global = i18n.Errorf("Invalid ID: %s", id) break } diff --git a/server/routes/novel.go b/server/routes/novel.go index 188d393..47b34fc 100644 --- a/server/routes/novel.go +++ b/server/routes/novel.go @@ -2,19 +2,19 @@ package routes import ( "fmt" + "net/http" "strconv" "strings" - "net/http" - "codeberg.org/vnpower/pixivfe/v2/core" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/session" ) func NovelPage(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if _, err := strconv.Atoi(id); err != nil { - return fmt.Errorf("Invalid ID: %s", id) + return i18n.Errorf("Invalid ID: %s", id) } novel, err := core.GetNovelByID(r, id) diff --git a/server/routes/novelseries.go b/server/routes/novelseries.go index 238d606..f30435f 100644 --- a/server/routes/novelseries.go +++ b/server/routes/novelseries.go @@ -8,12 +8,13 @@ import ( "net/http" "codeberg.org/vnpower/pixivfe/v2/core" + "codeberg.org/vnpower/pixivfe/v2/i18n" ) func NovelSeriesPage(w http.ResponseWriter, r *http.Request) error { id := GetPathVar(r, "id") if _, err := strconv.Atoi(id); err != nil { - return fmt.Errorf("Invalid ID: %s", id) + return i18n.Errorf("Invalid ID: %s", id) } series, err := core.GetNovelSeriesByID(r, id) @@ -28,7 +29,7 @@ func NovelSeriesPage(w http.ResponseWriter, r *http.Request) error { page := GetQueryParam(r, "p", "1") pageNum, err := strconv.Atoi(page) if err != nil || pageNum < 1 || pageNum > pageLimit { - return fmt.Errorf("Invalid Page") + return i18n.Errorf("Invalid Page Number: %d", pageNum) } // TODO should use token only if R-18/R-18G diff --git a/server/routes/settings.go b/server/routes/settings.go index 3c03af4..a3ce5cc 100644 --- a/server/routes/settings.go +++ b/server/routes/settings.go @@ -1,8 +1,6 @@ package routes import ( - "errors" - "fmt" "net/http" "regexp" "slices" @@ -10,6 +8,7 @@ import ( "codeberg.org/vnpower/pixivfe/v2/config" "codeberg.org/vnpower/pixivfe/v2/core" + "codeberg.org/vnpower/pixivfe/v2/i18n" "codeberg.org/vnpower/pixivfe/v2/server/proxy_checker" "codeberg.org/vnpower/pixivfe/v2/server/session" "codeberg.org/vnpower/pixivfe/v2/server/utils" @@ -24,7 +23,7 @@ func setToken(w http.ResponseWriter, r *http.Request) error { _, err := core.API_GET_UnwrapJson(r.Context(), URL, token) if err != nil { - return errors.New("Cannot authorize with supplied token.") + return i18n.Error("Cannot authorize with supplied token.") } // Make a test request to verify the token. @@ -35,14 +34,14 @@ func setToken(w http.ResponseWriter, r *http.Request) error { } if resp.StatusCode != 200 { - return errors.New("Cannot authorize with supplied token.") + return i18n.Error("Cannot authorize with supplied token.") } // CSRF token csrf := r_csrf.FindStringSubmatch(resp.Body)[1] if csrf == "" { - return errors.New("Cannot authorize with supplied token.") + return i18n.Error("Cannot authorize with supplied token.") } // Set the token @@ -51,7 +50,7 @@ func setToken(w http.ResponseWriter, r *http.Request) error { return nil } - return errors.New("You submitted an empty/invalid form.") + return i18n.Error("You submitted an empty/invalid form.") } func setImageServer(w http.ResponseWriter, r *http.Request) error { @@ -127,7 +126,7 @@ func setCookie(w http.ResponseWriter, r *http.Request) error { return nil } } - return fmt.Errorf("Invalid Cookie Name: %s", key) + return i18n.Errorf("Invalid Cookie Name: %s", key) } func setRawCookie(w http.ResponseWriter, r *http.Request) error { @@ -189,7 +188,7 @@ func SettingsPost(w http.ResponseWriter, r *http.Request) error { case "raw": err = setRawCookie(w, r) default: - err = errors.New("No such setting is available.") + err = i18n.Error("No such setting is available.") } if err != nil {