package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" urlpkg "net/url" "os" "path/filepath" "regexp" "strings" "time" ) func PlecPage(w http.ResponseWriter, r *http.Request) { body := `

PLEC Upload

Pick HTML files, give each an iteration name, then upload. The preview link is returned by the server.

HTML File Iteration Name
` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/plec", "PLEC Upload", body))) } func PlecPick(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() picked := PickFiles( "Select HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, cfg.LastPickDir, ) if len(picked) == 0 { writeJSON(w, map[string]any{"files": []any{}}) return } cfg.LastPickDir = filepath.Dir(picked[0]) _ = SaveConfig(cfg) files := make([]map[string]string, 0, len(picked)) for _, p := range picked { files = append(files, map[string]string{"path": p, "name": filepath.Base(p)}) } writeJSON(w, map[string]any{"files": files}) } type plecRow struct { ID string `json:"id"` Path string `json:"path"` Iteration string `json:"iteration"` } func PlecUpload(w http.ResponseWriter, r *http.Request) { var req struct { Rows []plecRow `json:"rows"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } cfg := LoadConfig().Plec type rowErr struct { ID string `json:"id"` Message string `json:"message"` } var rowErrors []rowErr var valid []plecRow htmlRx := regexp.MustCompile(`(?i)\.html?$`) for _, row := range req.Rows { if row.Path == "" || !fileExists(row.Path) { rowErrors = append(rowErrors, rowErr{row.ID, "File missing"}) } else if strings.TrimSpace(row.Iteration) == "" { rowErrors = append(rowErrors, rowErr{row.ID, "Iteration name required"}) } else if !htmlRx.MatchString(row.Path) { rowErrors = append(rowErrors, rowErr{row.ID, "Must be .html"}) } else { valid = append(valid, row) } } if len(rowErrors) > 0 { writeJSON(w, map[string]any{"rowErrors": rowErrors}) return } if len(valid) == 0 { writeJSON(w, map[string]any{"error": "Nothing to upload."}) return } stamp := time.Now().Format("20060102150405") var buf bytes.Buffer mw := multipart.NewWriter(&buf) for i, row := range valid { idx := i + 1 data, err := os.ReadFile(row.Path) if err != nil { writeJSON(w, map[string]any{"error": "Read failed: " + err.Error()}) return } stampedName := appendTimestamp(filepath.Base(row.Path), stamp) fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } if _, err := fw.Write(data); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", "")) // Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars. iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", "")) if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } } if err := mw.Close(); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } httpReq.Header.Set("Content-Type", mw.FormDataContentType()) httpReq.Header.Set("Origin", cfg.OriginUrl) httpReq.Header.Set("Referer", cfg.OriginUrl+"/") httpReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") httpReq.Header.Set("X-Requested-With", "XMLHttpRequest") httpReq.Header.Set("Accept", "*/*") resp, err := http.DefaultClient.Do(httpReq) if err != nil { writeJSON(w, map[string]any{"error": "Network error: " + err.Error()}) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { writeJSON(w, map[string]any{"error": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))}) return } var parsed map[string]any if err := json.Unmarshal(respBody, &parsed); err != nil { writeJSON(w, map[string]any{"error": "Server returned non-JSON:\n" + truncate(string(respBody), 800)}) return } var preview any if v, ok := parsed["preview"]; ok && v != nil { preview = v } else if v, ok := parsed["previewURL"]; ok { preview = v } writeJSON(w, map[string]any{"preview": preview, "raw": parsed, "rawText": string(respBody)}) } func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) { var req struct { Text string `json:"text"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } if err := CopyToClipboard(req.Text); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]any{"ok": true}) } func OpenEndpoint(w http.ResponseWriter, r *http.Request) { var req struct { URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } OpenInBrowser(req.URL) writeJSON(w, map[string]any{"ok": true}) } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil } func appendTimestamp(filename, stamp string) string { ext := filepath.Ext(filename) base := filename if ext != "" { base = filename[:len(filename)-len(ext)] } return base + "_" + stamp + ext } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] } // createFormFileWithType is like multipart.Writer.CreateFormFile but lets us // override the Content-Type (CreateFormFile hardcodes "application/octet-stream"). func createFormFileWithType(mw *multipart.Writer, field, filename, contentType string) (io.Writer, error) { h := make(map[string][]string) h["Content-Disposition"] = []string{ fmt.Sprintf(`form-data; name=%q; filename=%q`, field, filename), } h["Content-Type"] = []string{contentType} return mw.CreatePart(h) } // encodeURICompatible mimics JS encodeURI() — keeps A-Za-z0-9 and these // reserved/mark chars: ;,/?:@&=+$-_.!~*'()# func encodeURICompatible(s string) string { keep := func(c byte) bool { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') { return true } switch c { case ';', ',', '/', '?', ':', '@', '&', '=', '+', '$', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#': return true } return false } var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if keep(c) { b.WriteByte(c) } else { b.WriteString(fmt.Sprintf("%%%02X", c)) } } return b.String() }