Dup Ver Goto 📝

SimpleUrlShortener

PT2/lang/php/web does not exist
To
476 lines, 1592 words, 13121 chars Page 'SimpleUrlShortener' does not exist.

Simple url shortener backend

This is only intended for personal use, possibly giving urls to a few friends. Nothing that requires scaling.

This is made by Gemini using the following prompts.

The frontend then sends JSON to the backend of the form

{
    "short": "never",
    "url": "https://gonna.give.you.up/"
}

To delete, set the url to !delete.

Then one can write a simple frontend with Javascript.

CORS headers are sent in the cors.php file if needed (e.g. if the add short interface is on a different domain than the get short frontend, nice for security as the main user-facing frontend domain has no capacity to add or delete).

<?php
header("Access-Control-Allow-Origin: *"); // or specify your domain, e.g. "https://example.com"
header("Access-Control-Allow-Methods: POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json");

A backend file for a given table then needs to be merely

<?php
define('PREFIX','bobbins');
require('short_be.php');

A frontend script does

<?php
define('BACKEND',"never_gonna.php");
define('LIST_URL',"https://give.you.up/.list");
define('SHORT_URL_PREFIX','https://never.gonna.let.you.down.com/');
require_once("add_short_common.php");

1 Prompt for Gemini

A php class which manages shortened urls. The backend database connection credentials etc are in dbauth.php. Then there is a PREFIX defined for the tables. Table is named PREFIX_short_to_url. This table has two elements: short and url. If the url to be stored is !delete, then the short is deleted. A web client sends POST requests in json of the form { "short": "abc", "url": "https://domain/..." }. Domains main be local to the lan, so needn't contain a period. Either the url is a valid url, or else it is !delete. Anything else and an error is returned. The backend returns a json object, either { "message": "added abc => url", "short": short, "url": url }, or { "message": "deleted abc" } or { "error": "error message" . So a success is indicated by the presence of a "message" element, or else there is an "error" element. If there is a PHP error of some sort, the client should write this to the console. The top of the PHP backend has an easy way to enable/disable detailed error printing in the case of PHP errors. The backend database is Mysql.

2 Prompt for Gemini

The dbauth.php defines define('HOST',"..."); define('PORT',...); define('USER',"..."); define('PASS',"..."); define('DB',"...");

Example reporting errors (from Gemini)

fetch('api/index.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ short: 'nas', url: 'http://nas-server/storage' })
})
.then(response => {
    if (!response.ok) {
        // This catches 500 errors and logs the PHP output to console
        response.text().then(text => console.error("PHP Error:", text));
    }
    return response.json();
})
.then(data => {
    if (data.message) console.log("Success:", data.message);
    if (data.error) console.warn("App Error:", data.error);
});

table create (from Gemini)

CREATE TABLE IF NOT EXISTS `YOURPREFIX_short_to_url` ( `short` VARCHAR(191) NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY (`short`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

table create php (from Gemini)

<?php
// Enable error reporting for setup
ini_set('display_errors', 1);
error_reporting(E_ALL);

require_once("dbauth.php");

try {
    // 1. Connect to MySQL
    $mysqli = new mysqli(HOST, USER, PASS, DB, PORT);

    if ($mysqli->connect_error) {
        throw new Exception("Connection failed: " . $mysqli->connect_error);
    }

    // 2. Prepare the Table Name
    $tableName = PREFIX . "_short_to_url";

    // 3. Define the SQL
    // We use VARCHAR(191) for 'short' to ensure compatibility with utf8mb4 indexing
    $sql = "CREATE TABLE IF NOT EXISTS `$tableName` (
        `short` VARCHAR(191) NOT NULL,
        `url` TEXT NOT NULL,
        PRIMARY KEY (`short`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";

    // 4. Execute
    if ($mysqli->query($sql) === TRUE) {
        echo "<h3>Success!</h3>";
        echo "Table <strong>$tableName</strong> is ready.";
    } else {
        throw new Exception("Error creating table: " . $mysqli->error);
    }

    $mysqli->close();

} catch (Exception $e) {
    echo "<h3>Setup Failed</h3>";
    echo "Error: " . $e->getMessage();
}

PHP Backend

<?php
/**
 * Error Reporting Toggle
 */
$DEBUG_MODE = true; 

if ($DEBUG_MODE) {
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);
} else {
    ini_set('display_errors', 0);
    error_reporting(0);
}

// Security and Credentials
require_once("cors.php");
require_once("bouncer.php");
require_once("dbauth.php"); 

header('Content-Type: application/json');

/**
 * URL Manager Class
 */
class UrlManager {
    private $db;
    private $table;

    public function __construct() {
        // Using the constants defined in dbauth.php
        $this->db = new mysqli(HOST, USER, PASS, DB, PORT);

        if ($this->db->connect_error) {
            throw new Exception("Connection failed: " . $this->db->connect_error);
        }

        // table name: PREFIX_short_to_url
        $this->table = PREFIX . "_short_to_url";
    }

    public function handleRequest($jsonInput) {
        $data = json_decode($jsonInput, true);

        // Validation: Basic structure
        if (!$data || !isset($data['short']) || !isset($data['url'])) {
            return ["error" => "Invalid JSON: 'short' and 'url' fields required."];
        }

        $short = trim($data['short']);
        $url = trim($data['url']);

        if (empty($short)) {
            return ["error" => "The 'short' value cannot be empty."];
        }

        // Action: Delete
        if ($url === "!delete") {
            return $this->deleteShort($short);
        }

        // Action: Add/Update
        // Validates protocol; accepts LAN domains (no period required)
        if (preg_match('~^(?:f|ht)tps?://~i', $url)) {
            return $this->upsertShort($short, $url);
        }

        return ["error" => "Invalid URL format or command."];
    }

    private function upsertShort($short, $url) {
        $sql = "INSERT INTO {$this->table} (short, url) VALUES (?, ?) 
                ON DUPLICATE KEY UPDATE url = VALUES(url)";

        $stmt = $this->db->prepare($sql);
        $stmt->bind_param("ss", $short, $url);

        if ($stmt->execute()) {
            return [
                "message" => "added $short => $url",
                "short" => $short,
                "url" => $url
            ];
        }
        return ["error" => "DB Execution failed: " . $this->db->error];
    }

    private function deleteShort($short) {
        $stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE short = ?");
        $stmt->bind_param("s", $short);

        if ($stmt->execute()) {
            return ["message" => "deleted $short"];
        }
        return ["error" => "DB Execution failed: " . $this->db->error];
    }
}

/**
 * Execution
 */
try {
    $manager = new UrlManager();
    $rawInput = file_get_contents('php://input');
    $response = $manager->handleRequest($rawInput);
    echo json_encode($response);
} catch (Exception $e) {
    // If PHP crashes, this is caught here. 
    // Client logs response text to console.
    http_response_code(500);
    echo json_encode(["error" => $e->getMessage()]);
}

dbauth.php defines the constants for the database HOST, USER, PASS, DB, PORT. bouncer.php is arbitrary PHP code that determines if the request is authorised, and errors out if not.

add_short_common.php - Web frontend to set shorts

<?php
require_once("bouncer.php");
$qs = $_SERVER["QUERY_STRING"];
if( $qs != "" ) {
  $qs = urldecode($qs);
  $qs = preg_replace("@['\"]@","",$qs);
  $val = "value='$qs'";
  $af1 = "";
  $af2 = "autofocus";
} else {
  $val = "";
  $af1 = "autofocus";
  $af2 = "";
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>URL Shortener</title>
<style>
body { font-family: sans-serif; max-width: 400px; margin: 2em auto; }
label { display: block; margin-top: 1em; }
input { width: 100%; padding: 0.5em; }
button { margin-top: 1em; padding: 0.5em 1em; }
#message { margin-top: 1em; font-weight: bold; }
</style>
</head>
<body>

<h1>Add a Short URL</h1>

<label for="short">Short code:</label>
<input type="text" id="short" placeholder="e.g. abc123, leave blank for auto generate" <?php echo $af1." ".$val; ?>>

<label for="url">Full URL:</label>
<input type="text" id="url" placeholder="e.g. https://example.com/page" <?php echo $af2; ?>>

<button id="submit">Save</button>
<div id="message"></div>

<script>
function getmods(e) {
  let t = ""
  if( e.shiftKey ) t = "S-"+t
  if( e.metaKey ) t = "M-"+t
  if( e.ctrlKey ) t = "C-"+t
  if( e.altKey ) t = "A-"+t
  return t
}

function acms(e) {
    let key = e.key
    const code = e.code.toLowerCase()
    if( code === "backquote" ) { key = "`" }
    if( code.startsWith("digit") ) { key = code.substr(5) }
    if( code === "space" ) { key = "space" }
    if( key === "-" ) { key = "minus" }
    if( key === "_" ) { key = "minus" }
    if( key === "+" ) { key = "equals" }
    let t = key.toLowerCase()
    t = getmods(e)+t
    console.log("acms",t)
    return t
}

window.addEventListener("keydown", e => {
  const k = acms(e)
  switch(k) {
    case "C-s":
      e.preventDefault()
      return dosubmit()
    case "A-l":
      e.preventDefault()
      return dolist()
  }
})

document.getElementById("submit").addEventListener("click", dosubmit)

function dolist() {
  window.location.href = "<?php echo LIST_URL; ?>"
}

async function dosubmit() {
  const short = document.getElementById("short").value.trim()
  const url = document.getElementById("url").value.trim()
  const message = document.getElementById("message")

  message.textContent = ""

  if (!url) {
    message.textContent = "Please fill in the url."
    return
  }
  if( url === "!delete" ) {
    await do_delete(short)
  } else {
    await do_insert(short,url)
  }
}

async function do_insert(short,url) {
  const message = document.getElementById("message")
  try {
    const reqbody = JSON.stringify({ short, url })
    console.log(reqbody)
    const response = await fetch("<?php echo BACKEND; ?>", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: reqbody
    })

    const text = await response.text()
    console.log(text)
    const data = JSON.parse(text)
    console.log({data})

    if( data.short ) {
      if( data.short === "#fail" ) {
        message.textContent = "#fail"
      } else {
        const short = data.short
        const short_url = `<?php echo SHORT_URL_PREFIX; ?>${short}`
        message.textContent = ""
        let span = document.createElement("span")
        span.textContent = short
        message.append(span)
        message.append(document.createTextNode(" "))
        let a = document.createElement("a")
        a.textContent = short_url
        a.href = short_url
        message.append(a)
      }
    } else {
      message.textContent = data.message || "Done."
    }
  } catch (err) {
    message.textContent = "Error: " + err
  }
}

async function do_delete(short) {
  const message = document.getElementById("message")
  try {
    const reqbody = JSON.stringify({ short, url:"!delete" })
    console.log(reqbody)
    const response = await fetch("insert_short.php", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: reqbody
    })

    const text = await response.text()
    console.log(text)
    const data = JSON.parse(text)
    console.log({data})

    if( data.message ) {
      message.textContent = data.message || "Done."
    } else {
      message.textContent = "no message"
    }
  } catch (err) {
    message.textContent = "Error: " + err
  }
}
</script>

</body>
</html>

Redirector

<?php
require_once("db.php");

$conn = new mysqli(HOST,USER,PASS,DB);
if ($conn->connect_error) {
    die("Connection failed: " . $conn->connect_error);
}

$stmt = $conn->prepare("SELECT id, short, url FROM ".PREFIX."_short_to_url WHERE short = ?");
$stmt->bind_param("s", $req);
$stmt->execute();
$result = $stmt->get_result();

if ($row = $result->fetch_assoc()) {
  $url = $row["url"];
  $url = str_replace("%s",$qstring,$url);
} else {
  $url = null;
}

$stmt->close();
$conn->close();
<?php
require_once("db.php");
require_once("query_core.php");

if( !is_null($url) ) {
  if (filter_var($url, FILTER_VALIDATE_URL)) {
    header("Location: " . $url, true, 302);  // 302 = temporary redirect
    exit();
  } else {
    http_response_code(500);
    $message = "Stored URL is invalid. '$url'";
    $message_class = "error";
  }
} else {
  $message = "No URL found for short code '$req'";
  $message_class = "notfound";
}