package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/cookiejar" 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 them to PLEC.

Inputs

or drop HTML files or folders here
(no files selected)
` 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}) } func PlecPickFolder(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() dir := PickFolder("Select folder", cfg.LastPickDir) if dir == "" { writeJSON(w, map[string]any{"files": []any{}}) return } cfg.LastPickDir = dir _ = SaveConfig(cfg) paths := collectHTMLFiles(dir) files := make([]map[string]string, 0, len(paths)) for _, p := range paths { 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) { send := newJSONStream(w) var req struct { Rows []plecRow `json:"rows"` Server string `json:"server"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { send(map[string]any{"type": "error", "message": 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 { send(map[string]any{"type": "rowErrors", "rowErrors": rowErrors}) return } if len(valid) == 0 { send(map[string]any{"type": "error", "message": "Nothing to upload."}) return } if req.Server == "wordpress" { plecUploadWordPress(send, cfg, valid) } else { plecUploadLegacy(send, cfg, valid) } } func plecUploadLegacy(send func(any), cfg PlecConfig, valid []plecRow) { stamp := time.Now().Format("20060102150405") var buf bytes.Buffer mw := multipart.NewWriter(&buf) for i, row := range valid { idx := i + 1 send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filepath.Base(row.Path))}) data, err := os.ReadFile(row.Path) if err != nil { send(map[string]any{"type": "error", "message": "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 { send(map[string]any{"type": "error", "message": err.Error()}) return } if _, err := fw.Write(data); err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } iter := encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", "")) if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } } if err := mw.Close(); err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))}) httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf) if err != nil { send(map[string]any{"type": "error", "message": 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 { send(map[string]any{"type": "error", "message": "Network error: " + err.Error()}) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { send(map[string]any{"type": "error", "message": 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 { send(map[string]any{"type": "error", "message": "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 } send(map[string]any{"type": "done", "preview": preview, "raw": parsed, "rawText": string(respBody)}) } func wpAuthenticate(send func(any), baseUrl, username, password string) (*http.Client, string, error) { jar, _ := cookiejar.New(nil) client := &http.Client{Jar: jar} // Pre-seed the test cookie WordPress checks for testURL, _ := urlpkg.Parse(baseUrl) jar.SetCookies(testURL, []*http.Cookie{{Name: "wordpress_test_cookie", Value: "WP Cookie check"}}) send(map[string]any{"type": "status", "message": "Authenticating with backup server..."}) form := urlpkg.Values{ "log": {username}, "pwd": {password}, "wp-submit": {"Log In"}, "redirect_to": {"/wp-admin/"}, "testcookie": {"1"}, } loginReq, _ := http.NewRequest("POST", baseUrl+"/wp-login.php", strings.NewReader(form.Encode())) loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") loginReq.Header.Set("Origin", baseUrl) loginReq.Header.Set("Referer", baseUrl+"/wp-login.php") loginReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") loginResp, err := client.Do(loginReq) if err != nil { return nil, "", fmt.Errorf("login request failed: %w", err) } io.ReadAll(loginResp.Body) loginResp.Body.Close() // After following redirects, check final URL — wp-admin means success if strings.Contains(loginResp.Request.URL.String(), "wp-login") { return nil, "", fmt.Errorf("WordPress login failed — check wpUsername / wpPassword in settings") } send(map[string]any{"type": "status", "message": "Fetching upload token..."}) pageReq, _ := http.NewRequest("GET", baseUrl+"/file-upload/", nil) pageReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") pageResp, err := client.Do(pageReq) if err != nil { return nil, "", fmt.Errorf("nonce fetch failed: %w", err) } pageBody, _ := io.ReadAll(pageResp.Body) pageResp.Body.Close() nonceRx := regexp.MustCompile(`"nonce"\s*:\s*"([a-f0-9]+)"`) m := nonceRx.FindSubmatch(pageBody) if m == nil { return nil, "", fmt.Errorf("could not extract nonce from backup server page") } return client, string(m[1]), nil } func plecUploadWordPress(send func(any), cfg PlecConfig, valid []plecRow) { const baseUrl = "http://20.255.60.183" if cfg.WpUsername == "" || cfg.WpPassword == "" { send(map[string]any{"type": "error", "message": "WordPress credentials not set. Configure wpUsername / wpPassword in settings."}) return } client, nonce, err := wpAuthenticate(send, baseUrl, cfg.WpUsername, cfg.WpPassword) if err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } var buf bytes.Buffer mw := multipart.NewWriter(&buf) _ = mw.WriteField("action", "plec_upload_files") _ = mw.WriteField("nonce", nonce) type rowMeta struct { Field string `json:"field"` IterationName string `json:"iterationName"` } var rowsMeta []rowMeta for i, row := range valid { idx := i + 1 fieldName := fmt.Sprintf("file_%d", idx) sanitized := strings.ReplaceAll(row.Iteration, " ", "") filename := sanitized + ".html" send(map[string]any{"type": "status", "message": fmt.Sprintf("Preparing %d/%d %s", idx, len(valid), filename)}) data, err := os.ReadFile(row.Path) if err != nil { send(map[string]any{"type": "error", "message": "Read failed: " + err.Error()}) return } fw, err := createFormFileWithType(mw, fieldName, filename, "text/html") if err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } if _, err := fw.Write(data); err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } rowsMeta = append(rowsMeta, rowMeta{Field: fieldName, IterationName: sanitized}) } rowsJSON, _ := json.Marshal(rowsMeta) _ = mw.WriteField("rows", string(rowsJSON)) if err := mw.Close(); err != nil { send(map[string]any{"type": "error", "message": err.Error()}) return } send(map[string]any{"type": "status", "message": fmt.Sprintf("Uploading %d file%s...", len(valid), pluralS(len(valid)))}) httpReq, _ := http.NewRequest("POST", baseUrl+"/wp-admin/admin-ajax.php", &buf) httpReq.Header.Set("Content-Type", mw.FormDataContentType()) httpReq.Header.Set("Origin", baseUrl) httpReq.Header.Set("Referer", baseUrl+"/file-upload/") httpReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") httpReq.Header.Set("X-Requested-With", "XMLHttpRequest") httpReq.Header.Set("Accept", "*/*") resp, err := client.Do(httpReq) if err != nil { send(map[string]any{"type": "error", "message": "Network error: " + err.Error()}) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { send(map[string]any{"type": "error", "message": 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 { send(map[string]any{"type": "error", "message": "Server returned non-JSON:\n" + truncate(string(respBody), 800)}) return } var preview string if data, ok := parsed["data"].(map[string]any); ok { if files, ok := data["files"].([]any); ok && len(files) > 0 { params := urlpkg.Values{"theme": {"calcite"}} nonceRx := regexp.MustCompile(`(?i)\.html?$`) for i, f := range files { if fm, ok := f.(map[string]any); ok { orig, _ := fm["original"].(string) saved, _ := fm["saved"].(string) n := nonceRx.ReplaceAllString(orig, "") params.Set(fmt.Sprintf("n%d", i+1), n) params.Set(fmt.Sprintf("m%d", i+1), saved) } } preview = baseUrl + "/preview?" + params.Encode() } } if preview == "" { if v, ok := parsed["preview"].(string); ok { preview = v } else if v, ok := parsed["previewURL"].(string); ok { preview = v } } send(map[string]any{"type": "done", "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() }