Dup Goto 📝

Simple Web-based Clipboard in PHP

To Pop
435 lines, 1144 words, 10457 chars Monday 2024-05-20 20:27:11

Having multiple machines, and needing an easy way to copy and paste between them, sometimes not even on the same LAN, I wrote a PHP backend for the database used in NetworkClipboard2 so that I could have something web-visible using a simple shared web host with a LAMP stack. The database is set up with the same SQL as before (two tables: pointers and clips).

Web Backend (PHP)

The bouncer.php contains the security code, which bails immediately with an error if the user is not authorised. There is a simple web interface (not included in this page) that uses essentially the same database PHP and dumps a simple formatted HTML page. Anything more complicated will use fetch and the backend below.

get

<?php
require_once("bouncer.php");
require_once("db.php"); # database credentials
require_once("jsonetc.php");
must_post();

$request = get_json_from_post();
if( !isset($request["name"]) ) {
  serve_error_json("noname","No name",400);
}
$name = $request["name"];
if( !preg_match('/^[A-Za-z0-9_]+$/',$name) ) {
  serve_error_json("invalidname","Invalid name",400);
}
if( !isset($request["namespace"]) ) {
  $namespace = "_";
} else {
  $namespace = $request["namespace"];
}
if( !preg_match('/^[A-Za-z0-9_]+$/',$namespace) ) {
  serve_error_json("invalidnamespace","Invlaid namespace",400);
}

$conn = new mysqli($host, $user, $pass, $db);

// Check connection
if ($conn->connect_error) {
  serve_error_json("connerror","SQL Connect Error",500);
}

$sql = "SELECT ${prefix}_pointers.clipid, ${prefix}_clips.value, ${prefix}_clips.time 
  FROM ${prefix}_pointers JOIN ${prefix}_clips 
  ON (${prefix}_pointers.clipid = ${prefix}_clips.id) 
  WHERE (
    ${prefix}_pointers.namespace = ? AND
    ${prefix}_pointers.name = ?
  )";
$stmt = $conn->prepare($sql);
$stmt->bind_param("ss", $namespace,$name);
$error = false;
$found = false;
$output = null;
if( !$stmt->execute() ) {
  serve_error_json("sqlerror","SQL Error",500);
}
$result = $stmt->get_result(); 
while($row = $result->fetch_assoc()) {
  $output = $row['value'];
  break;
}
$conn->close();
if( $output == null ) {
  serve_json([
    "namespace" => $namespace,
    "name" => $name,
    "message" => "not found",
  ],404);
}
serve_json([
  "namespace" => $namespace,
  "name" => $name,
  "message" => "success",
  "value" => $output
],200);

put

<?php
//ini_set('display_errors', 1);
//error_reporting(E_ALL);
require_once("bouncer.php");
require_once("db.php");
require_once("jsonetc.php");
must_post();

$append = false;
if( isset($_GET['append']) ) {
  $append = $_GET['append'] == "y";
}

$request = get_json_from_post();
if( !isset($request["name"]) ) {
  serve_error_json("noname","No name",400);
}
$name = $request["name"];
if( !preg_match('/^[A-Za-z0-9_]+$/',$name) ) {
  serve_error_json("invalidname","Invalid name",400);
}
if( !isset($request["namespace"]) ) {
  $namespace = "_";
} else {
  $namespace = $request["namespace"];
}
if( !preg_match('/^[A-Za-z0-9_]+$/',$namespace) ) {
  serve_error_json("invalidnamespace","Invlaid namespace",400);
}
if( !isset($request["value"]) ) {
  serve_error_json("novalue","No value",400);
}
$value = $request["value"];

$conn = new mysqli($host, $user, $pass, $db);

// Check connection
if ($conn->connect_error) {
  serve_error_json("connerror","SQL Connect Error",500);
}

$prev = "";
if( $append ) {
  $sql = "SELECT ${prefix}_pointers.clipid, ${prefix}_clips.value, ${prefix}_clips.time 
    FROM ${prefix}_pointers JOIN ${prefix}_clips 
    ON (${prefix}_pointers.clipid = ${prefix}_clips.id) 
    WHERE (
      ${prefix}_pointers.namespace = ? AND
      ${prefix}_pointers.name = ?
    )";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param("ss", $namespace,$name);
  $output = null;
  if( !$stmt->execute() ) {
    serve_error_json("sqlerror","SQL Error",500);
  }
  $result = $stmt->get_result(); 
  while($row = $result->fetch_assoc()) {
    $output = $row['value'];
    break;
  }
  if( !is_null($output) ) {
    $prev = $output;
    if( !substr($output,-1) === "\n" ) {
      $prev .= "\n";
    }
  }
}
$value = $prev.$value;

$sql1 = $conn->prepare("START TRANSACTION");
$sql2 = $conn->prepare("INSERT INTO ${prefix}_clips 
  (namespace,name,value) VALUES ( ?, ?, ? )");
$sql2->bind_param("sss",$namespace,$name,$value);
$sql3 = $conn->prepare("SET @a = (SELECT LAST_INSERT_ID())");
$sql4 = $conn->prepare("INSERT INTO ${prefix}_pointers (namespace,name,clipid) 
  VALUES ( ?, ?, @a )
  ON DUPLICATE KEY UPDATE clipid=(@a)");
$sql4->bind_param("ss",$namespace,$name);
$sql5 = $conn->prepare("COMMIT");

if( !$sql1->execute() ) { 
  serve_error_json("sqlerror","SQL Error sql1",500);
}
if( !$sql2->execute() ) { 
  serve_error_json("sqlerror","SQL Error sql2",500);
}
if( !$sql3->execute() ) { 
  serve_error_json("sqlerror","SQL Error sql3",500);
}
if( !$sql4->execute() ) { 
  serve_error_json("sqlerror","SQL Error sql4",500);
}
if( !$sql5->execute() ) { 
  serve_error_json("sqlerror","SQL Error sql5",500);
}
$conn->close();
serve_json([
  "namespace" => $namespace,
  "name" => $name,
  "message" => "inserted"
],200);

list

<?php

require_once("bouncer.php");
require_once("db.php");
require_once("jsonetc.php");
must_post();

$request = get_json_from_post();
if( !isset($request["name"]) ) {
  serve_error_json("noname","No name",400);
}
$pattern = $request["name"];
if( preg_match('@/@',$pattern) ) {
  serve_error_json("invalidname","Invalid name",400);
}
if( !isset($request["namespace"]) ) {
  $namespace = "_";
} else {
  $namespace = $request["namespace"];
}
if( !preg_match('/^[A-Za-z0-9_]+$/',$namespace) ) {
  serve_error_json("invalidnamespace","Invlaid namespace",400);
}

$conn = new mysqli($host, $user, $pass, $db);

// Check connection
if ($conn->connect_error) {
  serve_error_json("connerror","SQL Connect Error",500);
}

$sql1 = "SELECT name FROM ${prefix}_pointers WHERE namespace = ?";
$stmt1 = $conn->prepare($sql1);
$stmt1->bind_param("s", $namespace);
$error = false;
$found = false;
$names = [];
if( !$stmt1->execute() ) {
  http_status_code(500);
  die("stmt1");
}
$result = $stmt1->get_result(); 
while($row = $result->fetch_assoc()) {
  $x = $row['name'];
  if( @preg_match("/$pattern/",$x) ) {
    array_push($names,"$namespace:$x");
  }
}
if( count($names) == 0 ) {
  serve_json([
    "namespace" => $namespace,
    "name" => $pattern,
    "message" => "not found",
  ],404);
}
serve_json([
  "namespace" => $namespace,
  "pattern" => $pattern,
  "message" => "success",
  "names" => $names
],200);

$conn->close();

CLI Frontend (Python)

Very quick and dirty, certainly the antithesis of proper software engineering, but does everything I need, so here it is.

eget

#!/usr/bin/env python
import sys
import os
import requests
import bouncer
import json
from icecream import ic

url = "..."

def eprint(*xs,**kw):
  kw['file'] = sys.stderr
  print(*xs,**kw)

args = sys.argv[1:]
if len(args) == 0:
  eprint(f"{sys.argv[0]} <name> [<name>...]")
  exit(1)

def getname(arg):
  if ":" in arg:
    namespace, name = arg.split(":",1)
  else:
    name = arg
    namespace = os.getenv("CLIPNS","_")
  return (namespace,name)

cookies = { bouncer.cookiename: bouncer.cookievalue }

for arg in args:
  namespace, name = getname(arg)
  req = { "namespace": namespace, "name": name }
  j = json.dumps(req)

  r = requests.post(url,data=j,cookies=cookies)
  if r.status_code == 200:
    try:
      b = json.loads(r.text)
      value = b["value"]
      value = value.replace("\r","")
      print(value)
    except Exception as e:
      eprint("error",r,r.text,e,file=sys.stderr)
  if r.status_code == 404:
    eprint(f"{namespace}:{name} not found",file=sys.stderr)
    exit(1)
  if r.status_code == 403:
    eprint(f"Access denied",file=sys.stderr)
    exit(1)

eput

#!/usr/bin/env python
import sys
import os
import requests
import bouncer
import json
from icecream import ic

url = "..."

def eprint(*xs,**kw):
  kw['file'] = sys.stderr
  print(*xs,**kw)

args = sys.argv[1:]
if len(args) == 0:
  eprint(f"{sys.argv[0]} <name> [<filename>...]")
  eprint(f"Uses standard input for -, or if no filenames provided")
  exit(1)

def getname(arg):
  if ":" in arg:
    namespace, name = arg.split(":",1)
  else:
    name = arg
    namespace = os.getenv("CLIPNS","_")
  return (namespace,name)

cookies = { bouncer.cookiename: bouncer.cookievalue }

namespace, name = getname(args.pop(0))
if len(args) == 0:
  args = ["-"]
value = []

for arg in args:
  if arg == "-":
    value.append(sys.stdin.read())
  else:
    if os.path.isfile(arg):
      with open(arg) as f:
        value.append(f.read())
    else:
      eprint(f"{arg} is not a file")
if len(value) == 0:
  eprint(f"No content")
value = "\n".join(value)
value = value.replace("\r","")

req = { "namespace": namespace, "name": name, "value": value }
j = json.dumps(req)

r = requests.post(url,data=j,cookies=cookies)
if r.status_code == 200:
  eprint(f"Inserted {namespace}:{name} ({len(value)} bytes)")
elif r.status_code == 400:
  eprint(f"Invalid request",file=sys.stderr)
  exit(1)
elif r.status_code == 500:
  eprint(f"Server error",file=sys.stderr)
  eprint(r.text,file=sys.stderr)
  exit(1)
elif r.status_code == 403:
  eprint(f"Access denied",file=sys.stderr)
  exit(1)
else:
  eprint("WTF",r.status_code,r.text)
  exit(1)

elist

#!/usr/bin/env python
import sys
import os
import requests
import bouncer
import json
from icecream import ic

url = "..."

def eprint(*xs,**kw):
  kw['file'] = sys.stderr
  print(*xs,**kw)

args = sys.argv[1:]
if len(args) == 0:
  eprint(f"{sys.argv[0]} <name> [<name>...]")
  exit(1)

def getname(arg):
  if ":" in arg:
    namespace, name = arg.split(":",1)
  else:
    name = arg
    namespace = os.getenv("CLIPNS","_")
  return (namespace,name)

cookies = { bouncer.cookiename: bouncer.cookievalue }

for arg in args:
  namespace, name = getname(arg)
  req = { "namespace": namespace, "name": name }
  j = json.dumps(req)

  r = requests.post(url,data=j,cookies=cookies)
  if r.status_code == 200:
    try:
      b = json.loads(r.text)
      if b["message"] == "success":
        print("\n".join(b["names"]))
      else:
        raise Exception("Didn't succeed")
    except Exception as e:
      eprint("error",r,r.text,e)
  if r.status_code == 404:
    eprint(f"{namespace}:{name} not found")
  if r.status_code == 403:
    eprint(f"Access denied")
    exit(1)