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 167.99.227.249 (Default) 20.255.60.183 (Backup) Select Folder Select File(s) Clear or drop HTML files or folders here (no files selected) FilenameIteration Name Upload Open Copy Link ` 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() }
Pick HTML files, give each an iteration name, then upload them to PLEC.