This commit is contained in:
2026-06-23 13:53:55 +08:00
parent c336f176e6
commit 331e49ea29
13 changed files with 557 additions and 28 deletions

View File

@@ -7,6 +7,7 @@ import (
"io"
"mime/multipart"
"net/http"
"net/http/cookiejar"
urlpkg "net/url"
"os"
"path/filepath"
@@ -23,7 +24,13 @@ func PlecPage(w http.ResponseWriter, r *http.Request) {
</header>
<section class="tool-panel">
<div class="panel-header"><h3 class="panel-title">Inputs</h3></div>
<div class="panel-header">
<h3 class="panel-title">Inputs</h3>
<select id="serverSelect">
<option value="legacy">167.99.227.249 (Default)</option>
<option value="wordpress">20.255.60.183 (Backup)</option>
</select>
</div>
<div class="panel-body">
<div class="control-group">
<div id="dropZone" class="drop-zone">
@@ -54,10 +61,15 @@ func PlecPage(w http.ResponseWriter, r *http.Request) {
<style>
.row-error { color:#f48771; font-size:12px; margin-top:4px; }
#rows input[type=text] { width: 100%; }
.panel-header { display: flex; align-items: center; }
#serverSelect { margin-left:auto; padding:3px 6px; font-size:12px; cursor:pointer; }
</style>
<script>
const PAGE_SIZE = 5;
const serverSelect = document.getElementById('serverSelect');
serverSelect.value = localStorage.getItem('plec.lastServer') || 'legacy';
serverSelect.addEventListener('change', () => localStorage.setItem('plec.lastServer', serverSelect.value));
let nextId = 1;
let rows = [];
let page = 0;
@@ -206,7 +218,7 @@ document.getElementById('upload').addEventListener('click', async () => {
const res = await fetch('/api/plec/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rows }),
body: JSON.stringify({ rows, server: serverSelect.value }),
});
await readJSONStream(res, (msg) => {
if (msg.type === 'status') {
@@ -299,7 +311,8 @@ type plecRow struct {
func PlecUpload(w http.ResponseWriter, r *http.Request) {
send := newJSONStream(w)
var req struct {
Rows []plecRow `json:"rows"`
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()})
@@ -335,6 +348,14 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
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
@@ -357,9 +378,7 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
send(map[string]any{"type": "error", "message": 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, " ", ""))
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
@@ -408,6 +427,171 @@ func PlecUpload(w http.ResponseWriter, r *http.Request) {
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"`