Initial import

This commit is contained in:
2026-06-04 11:13:34 +01:00
commit 563d9cc96d
143 changed files with 10572 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.PHONEY: all component live
all:
./node_modules/webpack/bin/webpack.js
component:
@if [ -z "$NAME" ]; then echo "Specify NAME="; exit 10 ; fi
@cp src/Template.vue src/${NAME}.vue
live: all
rsync -avPp * ../docman/
Executable
+172
View File
@@ -0,0 +1,172 @@
<?php
ini_set("display_errors", 1);
require_once(__DIR__ . "/lib/ErrorHandler.php");
if (!array_key_exists("SHELL", $_SERVER)) {
ErrorHandler::hook();
}
require_once(__DIR__ . "/lib/Session.php");
Session::init("memcache", 11211);
define('ROOT', __DIR__);
require_once(ROOT . "/vendor/autoload.php");
$args = array_merge($_GET, $_POST);
use \Michelf\Markdown;
use \Jenssegers\Blade\Blade;
_add_code_dir(ROOT . "/lib");
//$dir = opendir(ROOT . "/lib");
//while ($file = readdir($dir)) {
// if (str_ends_with($file, ".php")) {
// require_once(ROOT . "/lib/" . $file);
// }
//}
//closedir($dir);
DB::getInstance()->connect(
Config::get("DB_USER"),
Config::get("DB_PASS"),
Config::get("DB_HOST"),
Config::get("DB_DATABASE")
);
_add_code_dir(ROOT . "/app");
//$dir = opendir(ROOT . "/app");
//while ($file = readdir($dir)) {
// if (str_ends_with($file, ".php")) {
// require_once(ROOT . "/app/" . $file);
// }
//}
//closedir($dir);
require_once("lib/App.php");
require_once("routes/api.php");
require_once("routes/web.php");
Model::cache_connect("memcache", 11211);
if (!is_terminal()) {
$blade = new Blade(ROOT . '/views', ROOT . '/cache');
$req = new Request;
App::dispatch($req);
}
function is_terminal() {
return ($_SERVER['argc'] > 0);
}
function blade($name, $args = []) {
global $blade;
$args['flash_error'] = Session::get("flash_error");
$args['flash_warn'] = Session::get("flash_warn");
$args['flash_success'] = Session::get("flash_info");
$args['flash_info'] = Session::get("flash_success");
Session::unset("flash_error");
Session::unset("flash_warn");
Session::unset("flash_info");
Session::unset("flash_success");
return $blade->render($name, $args);
}
function get_user() {
$uid = Session::get("user");
if ($uid) {
return new User($uid);
}
return false;
}
function flash($type, $message) {
if ($_SERVER['argc'] == 0) {
Session::set("flash_" . $type, $message);
} else {
print("$type: $message\n");
}
}
function fmt_date($month, $year) {
if (($year < 1900) && ($year > 50)) {
$year = 1900 + $year;
}
if (($year < 1900) && ($year <= 50)) {
$year = 2000 + $year;
}
if ($month > 0) {
$dt = DateTime::createFromFormat("!m/Y", $month . "/" . $year);
return $dt->format("F Y");
} else {
$dt = DateTime::createFromFormat("!Y", $year);
return $dt->format("Y");
}
}
function format_size($s) {
if ($s < 102400) return sprintf("%.1fkB", $s / 1024);
if ($s < 10485760) return sprintf("%.1fMB", $s / (1024 * 1024));
return sprintf("%dMB", $s / (1024 * 1024));
}
function redirect($url) {
return [
302, "", [
"Location" => $url
]
];
// if ($_SERVER['argc'] == 0) {
// header("Location: $url");
// exit(0);
// }
}
function jsredirect($url) {
print('<script>document.location="' . $url . '";</script>');
exit(0);
}
function back() {
return redirect($_SERVER['HTTP_REFERER']);
}
function __unref($v) {
if ($v instanceof Model) {
return $v->id;
}
return $v;
}
function _add_code_dir($path) {
$dir = opendir($path);
while ($file = readdir($dir)) {
if (str_starts_with($file, ".")) {
continue;
}
if (is_dir($path . "/" . $file)) {
_add_code_dir($path . "/" . $file);
continue;
}
if (str_ends_with($file, ".php")) {
require_once($path . "/" . $file);
}
}
closedir($dir);
}
function strtolower_null($a) {
if (!is_string($a)) return $a;
return strtolower($a);
}
+66
View File
@@ -0,0 +1,66 @@
<?php
class CoverController {
static function get_cover($_request, $id, $filename, $size = false) {
$rev = new Revision($id);
if ($rev === false) return "";
$path = $rev->path();
$pdf = new PDF($path . "/doc.pdf");
if (!$pdf->exists()) {
return [404, "Not Found"];
}
if (!$size) {
$imgpath = sprintf("%s/cover.jpg", $path);
} else {
$imgpath = sprintf("%s/cover-%d.jpg", $path, $size);
}
$file = new File($imgpath);
if ($file->exists()) {
$h = $file->hash();
$h = "\"$h\"";
if ($h == $_request->header("If-None-Match")) {
return [304, "Not Modified", [
"ETag" => $h,
"Cache-Control" => "public, max-age=86400, must-revalidate",
]];
}
return new Image($file);
}
$cover = new Image($path . "/cover.jpg");
if (!$cover->exists()) {
$pdf->extract_page(0, $cover->path());
$cover = new Image($path . "/cover.jpg");
}
if (!$cover->exists()) {
return [404, "Not Found"];
}
if ($size === false) $size = $cover->width();
if ($size >= $cover->width()) {
$h = $cover->hash();
$h = "\"$h\"";
if ($h == $_request->header("If-None-Match")) {
return [304, "Not Modified", [
"ETag" => $h,
"Cache-Control" => "public, max-age=86400, must-revalidate",
]];
}
return $cover;
}
$cover->scale($size);
$img = $cover->save($imgpath, "image/jpeg");
return $img;
}
}
+561
View File
@@ -0,0 +1,561 @@
<?php
class DocumentController {
public static function browse($pid = 0) {
$product = new Product($pid);
return blade("documents", ["product" => $product]);
}
public static function show($id) {
$doc = new Document($id);
$doc->load();
return blade("document", [
"doc" => $doc
]);
}
public static function api_get($id) {
return new Document($id);
}
public static function api_set($id) {
$doc = new Document($id);
if ($doc->valid()) {
foreach ($_POST as $k=>$v) {
if ($k != "products") {
$doc->$k = trim($v);
}
}
$doc->save();
$pids = explode(",", $_POST['products']);
$dpl = DocProduct::find([["document", "=", $id]])->all();;
foreach ($dpl as $dp) {
$e = array_search($dp->product, $pids);
if ($e !== false) {
unset($pids[$e]);
continue;
}
$dp->delete();
}
foreach ($pids as $pid) {
$dp = new DocProduct;
$dp->document = $id;
$dp->product = $pid;
$dp->save();
}
$doc->cache_invalidate("products");
}
return $doc;
}
public static function api_get_products($id) {
$doc = new Document($id);
return $doc->products;
}
public static function api_move($id) {
$doc = new Document($id);
$from = $_POST['from'];
$to = $_POST['to'];
$dp = DocProduct([["document", "=", $id], ["product", "=", $from]])->first();
if ($dp) {
$dp->product = $to;
$dp->save();
}
$doc->cache_invalidate("products");
return [];
}
public static function del_docproduct($doc, $prod) {
$dp = DocProduct::find([["document", "=", $doc], ["product", "=", $prod]])->first();
if ($dp) {
$dp->delete();
}
return back();
}
public static function get_by_id($id) {
return Document::find([["internal_id", "=", trim($id)]])->first();
}
public static function api_merge($id) {
$from = new Document($id);
$to = new Document($_POST['to']);
if ($from->id == $to->id) return [];
if ($from && $to) {
$revs = Revision::find([["document", "=", $from->id]]);
for ($rev = $revs->first(); $rev = $revs->next(); ) {
$rev->document = $to->id;
$rev->save();
}
$from->delete();
}
return new Document($to->id); // Force refresh
}
public static function api_drag_drop($_request) {
$src_id = $_request->post("src_id");
$dst_id = $_request->post("dst_id");
$src_type = $_request->post("src_type");
$dst_type = $_request->post("dst_type");
$src_extra = $_request->post("src_extra");
$dst_extra = $_request->post("dst_extra");
$copy = $_request->post("copy") == "true";
if (($src_type == "document") && ($dst_type == "document")) {
// Merge src into dst
$src = new Document($src_id);
$dst = new Document($dst_id);
if ($src->id == $dst->id) return [];
foreach ($src->revisions as $r) {
$r->document = $dst->id;
$r->save();
}
$src->delete();
$dst->cache_invalidate("revisions");
}
if (($src_type == "document") && ($dst_type == "product")) {
if ($copy) {
$dp = new DocProduct();
$dp->document = $src_id;
$dp->product = $dst_id;
$dp->save();
} else {
// Move document into product
$dpl = DocProduct::find([["document", "=", $src_id], ["product", "=", $src_extra]])->all();
foreach ($dpl as $dp) {
$dp->product = $dst_id;
$dp->save();
}
$prod = new Product($src_extra);
$prod->cache_invalidate("documents");
}
$doc = new Document($src_id);
$doc->cache_invalidate("products");
$prod = new Product($dst_id);
$prod->cache_invalidate("documents");
}
if (($src_type == "product") && ($dst_type == "product")) {
if ($src_id == $dst_id) return [];
// Move product into product
$sp = new Product($src_id);
$dp = new Product($dst_id);
$sp->parent = $dp->id;
$sp->save();
$sp->cache_invalidate("parent");
$sp->cache_invalidate("children");
$dp->cache_invalidate("parent");
$dp->cache_invalidate("children");
}
return [200, ["didit" => "true"]];
}
public static function merge($id) {
$doc = new Document($id);
$proc = new Process("pdftk");
foreach ($doc->revisions as $rev) {
$proc->arg($rev->path() . "/doc.pdf");
}
$nr = new Revision;
$nr->document = $doc->id;
$nr->revno = "NEW";
$nr->save();
$out = new File($nr->path() . "/doc.pdf");
$out->parent()->mkdir();
$proc->arg("output");
$proc->arg((string)$out);
$r = $proc->execute();
if ($r != 0) {
print("<pre>");
print_r($proc->stderr());
print("</pre>");
exit(0);
}
return redirect("/document/" . $doc->id);
}
public static function create_overview($id, $move) {
$job = new GeminiJob($id, "document:$id");
$job->move = ($move == "move");
$jobid = $job->queue();
flash("success", "Job queued as ID " . $jobid);
return redirect("/document/" . $id);
}
public static function api_get_title_fragment($_request) {
$q = $_request->post("title");
$db = DB::getInstance();
$q1 = $db->query("
SELECT
DISTINCT title
FROM (
SELECT
DISTINCT title
FROM
document
WHERE
title LIKE :s
UNION SELECT
DISTINCT subtitle AS title
FROM
document
WHERE
subtitle LIKE :s
UNION SELECT
DISTINCT subsubtitle AS title
FROM
document
WHERE subsubtitle LIKE :s
) AS DERIVED", ["s" => $q . "%"]);
$o = $db->all($q1);
if ($o->count() == 0) {
return [404, "Not Found"];
}
if ($o->count() != 1) {
return [413, "Content Too Large"];
}
return $o[0]->title;
}
public static function separate($id) {
$doc = new Document($id);
$firstprod = $doc->products[0];
$count = 0;
foreach ($doc->revisions as $rev) {
$count++;
if ($count == 1) {
continue;
}
$newdoc = $doc->duplicate();
$newdoc->subsubtitle .= " - $count";
$rev->document = $newdoc->id;
$rev->save();
}
return redirect("/documents/" . $firstprod);
}
public static function api_get_metadata($id) {
return DocMeta::find([["document", "=", $id]])->orderBy("metadata")->all();
}
public static function api_new_metadata($_request, $id) {
$doc = new Document($id);
$doc->set_metadata($_request->put("item_id"), "");
return DocMeta::find([["document", "=", $id]])->orderBy("metadata")->all();
}
public static function api_set_metadata($_request, $id, $metadata) {
$doc = new Document($id);
$doc->set_metadata($metadata, $_request->post('data'));
return DocMeta::find([["document", "=", $id]])->orderBy("metadata")->all();
}
public static function api_delete_metadata($id, $metadata) {
$doc = new Document($id);
$doc->remove_metadata($metadata);
return DocMeta::find([["document", "=", $id]])->orderBy("metadata")->all();
}
public static function api_available_metadata($id) {
$exist = DocMeta::find([["document", "=", $id]])->orderBy("metadata")->all();
$metas = MetaType::find()->all();
$out = new Collection();
foreach ($metas as $meta) {
$e = false;
foreach ($exist as $ex) {
if ($ex->metadata == $meta->id) {
$e = true;
break;
}
}
if (!$e) {
$cl = new stdClass;
$cl->key = $meta->id;
$cl->value = $meta->name;
$out->push($cl);
}
}
$out->sort("value");
return $out;
}
public static function delete_metadata($id, $metadata) {
$doc = new Document($id);
$doc->remove_metadata($metadata);
return redirect("/document/" . $id);
}
public static function api_guess_docid($id) {
$doc = new Document($id);
$docid = $doc->guess_docid();
return new Collection(["id" => $id, "docid" => $docid]);
}
public static function download_attachment($id, $filename) {
$doc = new Document($id);
$atts = $doc->get_attachments();
foreach ($atts as $f) {
if ($f->basename() == $filename) {
$f->set_header("Content-Disposition", "attachment; filename=\"$filename\"");
return $f;
}
}
return false;
}
public static function upload_attachment($id) {
$doc = new Document($id);
return blade("upload.attachment", ["doc" => $doc]);
}
public static function do_upload_attachment($_request, $id) {
$doc = new Document($id);
mkdir(ROOT . "/attachments/" . $doc->id, 0777);
$f = 0;
while (($file = $_request->file("file", $f)) !== false) {
$nf = new File($file['tmp_name']);
$nf->rename(ROOT . "/attachments/" . $doc->id . "/" . $file['name']);
$f++;
}
return redirect("/document/" . $doc->id);
}
public static function upload_document($_request) {
return blade("upload.document");
}
public static function do_upload_document($_request) {
$i = 0;
while ($f = $_request->file("file", $i)) {
$file = new File($f['tmp_name']);
$sha256 = $file->hash();
$rev = Revision::find([["sha256", "=", $sha256]])->first();
if ($rev) {
return redirect("/uploads/duplicate/" . $rev->id);
}
$u = new Upload();
$u->filename = $f['name'];
$u->sha256 = $sha256;
$u->ocr = "N";
$u->ai = "N";
$u->owner = get_user()->id;
$u->save();
copy($f['tmp_name'], ROOT . "/uploads/" . $u->id . ".pdf");
$j = new ImportGeminiJob($u->id);
$j->queue();
$j = new ImportOCRJob($u->id);
$j->queue();
$i++;
}
return redirect("/uploads");
}
public static function rerun_upload($id) {
$u = new Upload($id);
if ($u->ocr == "F") {
$u->ocr = "N";
$u->save();
$j = new ImportOCRJob($u->id);
$j->queue();
}
if ($u->ai == "F") {
$u->ai = "N";
$u->save();
$j = new ImportGeminiJob($u->id);
$j->queue();
}
return redirect("/uploads");
}
public static function rerun_all() {
$ai = Upload::find([["ai", "=", "F"], ["owner", "=", get_user()->id]])->all();
foreach ($ai as $u) {
$u->ai = "N";
$u->save();
$j = new ImportGeminiJob($u->id);
$j->queue();
}
$ocr = Upload::find([["ocr", "=", "F"], ["owner", "=", get_user()->id]])->all();
foreach ($ocr as $u) {
$u->ocr = "N";
$u->save();
$j = new ImportOCRJob($u->id);
$j->queue();
}
return redirect("/uploads");
}
public static function uploads($_request) {
return DocumentController::uploads_page($_request, 0);
}
public static function uploads_page($_request, $page) {
$uploads = Upload::find([["owner", "=", get_user()->id]])->orderBy("id")->limit($page * 20, 20)->all();
return blade("upload.list", ["uploads" => $uploads, "page" => $page]);
}
public static function discard_upload($_request, $id) {
$u = new Upload($id);
if ($u->owner != get_user()->id) {
return [403, "Not yours, punk!"];
}
DB::getInstance()->query("delete from job where source=:source", ["source" => "upload:" . $u->id]);
unlink($u->getPDF());
$u->delete();
return redirect("/uploads");
}
public static function import($_request, $id, $source) {
$u = new Upload($id);
if ($u->owner != get_user()->id) {
return [403, "Not yours, punk!"];
}
$doc = false;
if ($source == "ai") {
$doc = Document::find_by_docid($u->ai_docid);
} else if ($source == "ocr") {
$doc = Document::find_by_docid($u->ocr_docid);
}
if (!$doc) {
$doc = new Document;
if ($source == "ai") {
$doc->internal_id = $u->ai_docid;
} else if ($source == "ocr") {
$doc->internal_id = $u->ocr_docid;
} else {
$doc->internal_id = "XX-XXXXX-XX";
}
$doc->title = $u->title;
$doc->subtitle = $u->subtitle;
$doc->subsubtitle = $u->subsubtitle;
$doc->overview = $u->overview;
$doc->save();
$incoming = Product::find([["title", "=", "Incoming"]])->first();
$dp = new DocProduct();
$dp->document = $doc->id;
$dp->product = $incoming->id;
$dp->save();
}
$rev = new Revision();
$rev->document = $doc->id;
$rev->revid="";
$rev->sha256 = $u->sha256;
$rev->origtitle = $u->source;
$rev->save();
$ocr = new OCR();
$ocr->revision = $rev->id;
$ocr->body = $u->body;
$ocr->save();
$out = new File($rev->path() . "/doc.pdf");
$out->parent()->mkdir();
$pdf = $u->getPDF();
$pdf->rename($out->path());
$db = DB::getInstance();
$db->query("DELETE FROM job WHERE source=:source", ["source" => "upload:" . $u->id]);
$u->delete();
$rev->info = $rev->getPDF()->info();
$rev->save();
$j = new IndexJob($rev->id);
$j->queue();
//return redirect("/uploads");
return redirect("/document/" . $doc->id);
}
public static function delete_upload($_request, $id) {
$u = new Upload($id);
if ($u->owner != get_user()->id) {
return [403, "Not yours, punk!"];
}
}
public static function upload_duplicate($id) {
$r = new Revision($id);
return blade("upload.duplicate", ["rev" => $r]);
}
public static function view_upload($id) {
$u = new Upload($id);
return $u->getPDF();
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
class DownloadController {
public static function get_download($id) {
return new DownloadJob($id);
}
public static function start_download() {
}
public static function api_downloads() {
$s = DownloadJob::find([["processed", "=", 0], ["owner", "=", get_user()->id]])->orderBy("queued")->limit(20);
return $s->all();
}
public static function api_add_download($_request) {
$url = $_request->put("url");
$d = new DownloadJob;
$d->url = $url;
$d->queued = time();
$d->started = 0;
$d->finished = 0;
$d->processed = 0;
$d->owner = get_user()->id;
$d->file = sprintf("download/file-%08X-%08X", rand(), time());
$d->save();
return DownloadController::api_downloads();
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
class FavouriteController {
public static function user_set_favourite($doc) {
$user = get_user();
if ($user) {
$user->set_favourite($doc);
}
return redirect("/document/" . $doc);
}
public static function user_unset_favourite($doc) {
$user = get_user();
if ($user) {
$user->unset_favourite($doc);
}
return redirect("/document/" . $doc);
}
public static function favourites() {
$user = get_user();
if (!$user) return [403, "Huh... yeah right"];
$f = $user->favourites();
$docs = new Collection;
foreach ($f as $fav) {
$doc = new Document($fav->document);
$docs->add($doc);
}
return blade("/favourites", ["docs" => $docs]);
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
class HomeController {
static function index() {
return blade("index");
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
class ImportController {
public static function downloads() {
return blade("downloads");
}
public static function imports() {
return blade("imports");
}
public static function api_imports() {
$is = ProcessJob::find([["imported", "=", 0]])->orderBy("queued")->limit(50)->all();
foreach ($is as $i) {
$i->load("revision");
}
return $is;
}
public static function api_delete_import($id) {
$d = new ProcessJob($id);
if ($d->valid()) {
$d->delete();
}
return ImportController::api_imports();
}
public static function api_set_import($_request, $id) {
$d = new ProcessJob($id);
if ($d->valid()) {
foreach ($_POST as $k=>$v) {
$d->$k = $v;
}
$d->save();
}
return ImportController::api_imports();
}
public static function api_add_document($_request, $id) {
$job = new ProcessJob($id);
$doc = Document::find([["internal_id", "=", $_request->put('internal_id')]])->first();
if (!$doc) {
$doc = new Document;
$doc->internal_id = trim($_request->put('internal_id'));
$doc->title = trim($_request->put('title'));
$doc->subtitle = trim($_request->put('subtitle'));
$doc->subsubtitle = trim($_request->put('subsubtitle'));
$doc->overview = trim($_request->put('overview'));
$doc->owner = get_user()->id;
$doc->save();
$prods = explode(",", $_request->put('products'));
foreach ($prods as $product) {
$dp = new DocProduct;
$dp->document = $doc->id;
$dp->product = $product;
$dp->save();
}
}
$job->document = $doc->id;
$rev = new Revision($job->revision);
$rev->document = $doc->id;
$rev->revno = trim($_request->put('revno'));
$rev->month = $_request->put('month');
$rev->year = $_request->put('year');
$rev->owner = get_user()->id;
$rev->save();
$job->imported = time();
$job->save();
return ImportController::api_imports();
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
class JobController {
public static function api_get_jobs($source) {
$db = DB::getInstance();
$q = $db->query("select * from job where source=:source order by queued", ["source" => $source]);
$c = new Collection();
while ($r = $db->nextRecord($q)) {
$c->push($r);
}
return $c;
}
public static function api_delete_job($id) {
$db = DB::getInstance();
$q = $db->query("select * from job where id=:id", ["id" => $id]);
$r = $db->nextRecord($q);
$q = $db->query("delete from job where id=:id", ["id" => $id]);
return JobController::api_get_jobs($r->source);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
class PDFController {
public static function download($_request, $id, $type, $filename) {
$rev = new Revision($id);
$path = $rev->path();
$rev->downloads++;
$rev->last_download = time();
$rev->save();
$disp = "attachment";
$mime = "application/octet-stream";
$file = null;
switch ($type) {
case "view":
$file = new PDF($path . "/doc.pdf");
break;
case "download":
$file = new PDF($path . "/doc.pdf");
$file->force_download();
break;
case "viewocr":
$file = new PDF($path . "/ocr.pdf");
break;
case "downloadocr":
$file = new PDF($path . "/ocr.pdf");
$file->force_download();
break;
}
$file->fake_filename($filename);
if ($file == null) {
return [404, "Not Found 1"];
}
if (!$file->exists()) {
return [404, "Not Found 2"];
}
$file->set_header("link", "<" . Config::get("URL") . "/pdf/" . $id . "/download/" . $filename . '>; rel="canonical"');
return $file;
}
public static function get_page($id, $page) {
$rev = new Revision($id);
$page = $rev->get_page($page, 150);
if ($page) {
return $page;
}
return [404, "Not Found"];
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
class ProductController {
public static function api_get_list($list) {
$db = DB::getInstance();
$pids = explode(",", $list);
$out = new Collection();
foreach ($pids as $pid) {
$q = $db->query("select * from product where id=:id", ["id" => $pid]);
if ($r = $db->nextRecord($q)) {
$out->push($r);
}
}
return $out;
}
public static function api_get_mru() {
$out = new Collection;
$mru = Session::get("mru");
if (!$mru) {
$mru = [];
}
foreach ($mru as $p) {
$prod = new Product($p);
if ($prod) {
$out->push($prod);
}
}
return $out;
}
public static function api_add_mru($id) {
$mru = Session::get("mru");
if (!$mru) {
$mru = [];
}
array_unshift($mru, $id);
$mru = array_unique($mru);
while (count($mru) > 5) {
array_pop($mru);
}
Session::set("mru", $mru);
return ProductController::api_get_mru();
}
public static function api_search() {
return Product::find([["full_path", "like", "%" . $_POST['search'] . "%"]])->orderBy("full_path")->all();
}
public static function api_add_child($_request, $id) {
$p = new Product;
$p->parent = $id;
$p->title = $_request->put("title");
$p->save();
return $p;
}
public static function api_set($id) {
$p = new Product($id);
$data = [];
foreach ($_POST as $k=>$v) {
$p->$k = $v;
}
$p->save();
return $p;
}
public static function api_move($id) {
$p = new Product($id);
$p->parent = $_POST['to'];
$p->save();
return $p;
}
public static function api_delete($id) {
$product = new Product($id);
if ($product->valid()) {
$laf = Product::find([["title", "=", "Lost and Found"]])->first();
DB::getInstance()->query("delete from docproduct where product=:pid", ["pid" => $product->id]);
foreach ($product->children as $c) {
$c->parent = $laf;
$c->save();
}
$product->load("parent");
$par = $product->parent;
$product->delete();
return $par;
}
return [];
}
public static function api_empty_trash($id) {
$prod = new Product($id);
$docs = $prod->documents;
foreach ($docs as $doc) {
$revs = $doc->revisions;
foreach ($revs as $rev) {
$rev->delete();
}
$dpl = DocProduct::find([["document", "=", $doc->id]])->all();
foreach ($dpl as $dp) {
$dp->delete();
}
$doc->delete();
}
$prod->invalidate("documents");
return [];
}
public static function api_available_metadata($id) {
$prod = new Product($id);
$exist = $prod->meta();
$metas = MetaType::find()->all();
$out = new Collection();
foreach ($metas as $meta) {
if (!in_array($meta->id, $exist)) {
$cl = new stdClass;
$cl->key = $meta->id;
$cl->value = $meta->name;
$out->push($cl);
}
}
$out->sort("value");
return $out;
}
public static function api_add_metadata($_request, $id) {
$prod = new Product($id);
$prod->add_meta($_request->put('item_id'));
return Collection::from_array($prod->meta());
}
public static function api_gemini_all($id, $move) {
$prod = new Product($id);
$c = new Collection();
foreach ($prod->documents as $doc) {
if ($prod->oneliner == "") {
$job = new GeminiJob($doc->id, "document:" . $doc->id);
$job->move = ($move == "move");
$jobid = $job->queue();
$c->push(["document" => $doc->id, "job" => $jobid]);
}
}
return $c;
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
class RevisionController {
public static function show($id) {
$rev = new Revision($id);
$rev->load("document");
if (!@$rev->info->Pages) {
$rev->info = $rev->getPDF()->info();
$rev->save();
}
return blade("revision", ["rev" => $rev]);
}
public static function api_set($id) {
$r = new Revision($id);
if ($r !== false) {
foreach ($_POST as $k=>$v) {
$r->$k = trim($v);
}
$r->save();
}
return $r;
}
public static function delete($id) {
$r = new Revision($id);
$r->load("document");
$doc = $r->document;
$r->delete();
return redirect("/document/" . $doc->id);
}
public static function redownload($id) {
$r = new Revision($id);
$j = new RedownloadJob($r->id, $r->origtitle, $r->path() . "/doc.pdf");
$jobid = $j->queue();
flash("success", "Job queued as ID " . $jobid);
return redirect("/revision/" . $id);
}
public static function purge($id) {
$r = new Revision($id);
$r->purge();
return redirect("/revision/" . $id);
}
public static function recompress($id) {
$r = new Revision($id);
$j = new RecompressJob($r->id);
$j->queue();
flash("info", "Recompression job queued");
return redirect("/revision/" . $id);
}
public static function ocr($id) {
$j = new OCRJob($id);
$j->queue();
return redirect("/revision/" . $id);
}
public static function rate_revision($id, $rating) {
$user = get_user();
if (!$user) {
return new Revision($id);
}
$db = DB::getInstance();
$db->query("INSERT INTO revision_rating (owner,revision,rating) values (:owner,:revision,:rating) ON DUPLICATE KEY UPDATE rating=:rating", [
"owner" => $user->id,
"revision" => $id,
"rating" => $rating
]);
$db->query("UPDATE revision SET rating=(SELECT AVG(rating) FROM revision_rating WHERE revision=:revision) WHERE id=:revision", ["revision" => $id]);
return new Revision($id);
}
public static function split_revision($id) {
$r = new Revision($id);
$r->load("document");
$doc = new Document();
$doc->title = $r->document->title;
$doc->subtitle = $r->document->subtitle;
$doc->subsubtitle = $r->document->subsubtitle;
$doc->overview = $r->document->overview;
$doc->oneliner = $r->document->oneliner;
$doc->internal_id = $r->document->internal_id;
$doc->save();
foreach ($r->document->products as $prod) {
$dp = new DocProduct();
$dp->document = $doc->id;
$dp->product = $prod->id;
$dp->save();
}
$r->document = $doc->id;
$r->save();
return redirect("/document/" . $doc->id);
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
class SearchController {
static public function api_title_search() {
$out = new Collection;
$q = DB::getInstance()->query("
select
id, internal_id, title, subtitle, subsubtitle,
match (internal_id, title, subtitle, subsubtitle, overview)
against (:search in boolean mode)
as rel
from
document
where
match (internal_id, title, subtitle, subsubtitle, overview)
against (:search in boolean mode)
order by
rel desc
limit
10
", ["search" => $_POST['search']]);
while ($r = DB::getInstance()->nextRecord($q)) {
$o = new Document($r->id);
$out->push($o);
}
return $out;
}
static public function search($page = 0) {
if (array_key_exists("search", $_POST)) {
$q = DB::getInstance()->query("
select
revision.id as id,
match(ocr.body) against (:search) as relevance
from
revision,ocr
where
match(ocr.body) against (:search) and
ocr.revision = revision.id and
not revision.document is null
order by
relevance desc
", ["search" => $_POST['search']]);
$slog = [];
while ($r = DB::getInstance()->nextRecord($q)) {
$slog[] = $r->id;
}
Session::set("search", json_encode($slog));
}
$rpp = 8;
$offset = $page * $rpp;
$out = [];
$slog = json_decode(Session::get("search"));
for ($i = 0; $i < $rpp; $i++) {
if ($offset + $i < count($slog)) {
$rev = new Revision($slog[$offset + $i]);
if ($rev) {
$out[] = $rev;
}
}
}
return blade("search", ["page" => $page, "count" => count($slog), "results" => $out, "pages" => ceil(count($slog) / $rpp)]);
}
}
+9
View File
@@ -0,0 +1,9 @@
<?php
class SitemapController {
public static function generate() {
$docs = Document::find()->all();
$revs = Revision::find()->all();
return blade("sitemap", ["docs" => $docs, "revs" => $revs]);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
class SpiderController {
public static function spider_pdfs() {
return blade("spider_pdfs");
}
public static function spider_pages() {
return blade("spider_pages");
}
public static function api_pdfs() {
$pdfs = Spider::find([["status", "=", "N"]])->orderBy("id")->limit(20)->all();
return $pdfs;
}
public static function api_pages() {
$pages = SpiderPage::find([["status", "=", "O"], ["title", "!=", ""]])->orderBy("title")->limit(20)->all();
return $pages;
}
public static function api_reject_pdf($id = null) {
if ($id == null) return [];
$i = explode(",", $id);
foreach ($i as $id) {
$pdf = new Spider($id);
if ($pdf) {
$pdf->status = "B";
$pdf->save();
}
}
return SpiderController::api_pdfs();
}
public static function api_accept_pdf($_request, $id = null) {
if ($id == null) return [];
$i = explode(",", $id);
foreach ($i as $id) {
$pdf = new Spider($id);
if ($pdf) {
$pdf->status = "D";
$pdf->save();
$job = new DownloadJob($pdf->url);
$job->queue(get_user()->id);
}
}
return SpiderController::api_pdfs();
}
public static function api_reject_page($_request, $id) {
$i = explode(",", $id);
foreach ($i as $id) {
$page = new SpiderPage($id);
if ($page) {
$page->status = "B";
$page->save();
}
}
return SpiderController::api_pages();
}
public static function api_accept_page($id) {
$i = explode(",", $id);
foreach ($i as $id) {
$page = new SpiderPage($id);
if ($page) {
$page->status = "N";
$page->save();
}
}
return SpiderController::api_pages();
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
class SystemController {
public static function status() {
$status = [
"B" => "Blacklisted",
"N" => "Pending",
"F" => "Failed",
"D" => "Done",
"Y" => "Done",
"W" => "Postponed",
"P" => "Processing",
"X" => "Deleted",
"O" => "Off-site",
"Q" => "Postponed",
"R" => "Redirect",
];
$q = DB::getInstance()->query("select count(id) as c, status from pages group by status order by status");
$spider = [];
while ($r = DB::getInstance()->nextRecord($q)) {
$spider[@$status[$r->status]] = $r->c;
}
$q = DB::getInstance()->query("select count(id) as c, status from spider group by status order by status");
$pdf = [];
while ($r = DB::getInstance()->nextRecord($q)) {
$pdf[@$status[$r->status]] = $r->c;
}
$q = DB::getInstance()->query("select count(id) as c, ocr from revision group by ocr order by ocr");
$ocr = [];
while ($r = DB::getInstance()->nextRecord($q)) {
$ocr[@$status[$r->ocr]] = $r->c;
}
$q = DB::getInstance()->query("select count(id) as c, idx from revision group by idx order by idx");
$idx = [];
while ($r = DB::getInstance()->nextRecord($q)) {
$idx[@$status[$r->idx]] = $r->c;
}
return blade("status", ["spider" => $spider, "pdf" => $pdf, "ocr" => $ocr, "idx" => $idx]);
}
public static function api_get_idmatches() {
return IDMatch::find([["id", ">=", 0]])->orderBy("weight")->all();
}
public static function api_add_idmatch($_request) {
$i = new IDMatch;
$i->example = $_request->put('example');
$i->regex = $_request->put('regex');
$i->weight = $_request->put('weight');
$i->save();
return SystemController::api_get_idmatches();
}
public static function api_set_idmatch($id) {
$i = new IDMatch($id);
if ($i) {
$i->example = $_POST['example'];
$i->regex = $_POST['regex'];
$i->weight = $_POST['weight'];
$i->save();
}
return SystemController::api_get_idmatches();
}
public static function api_del_idmatch($id) {
$i = new IDMatch($id);
if ($i) {
$i->delete();
}
return SystemController::api_get_idmatches();
}
public static function systems() {
$u = get_user();
$sys = System::find([["owner", "=", $u->id]])->all();
return blade("systems", ["systems" => $sys]);
}
public static function system($id) {
$sys = new System($id);
$sys->load("documents");
return blade("system", ["sys" => $sys]);
}
public static function systems_add() {
return blade("systems_add");
}
public static function systems_do_add($_request) {
$sys = new System;
$sys->name = $_request->post("name");
$sys->model = $_request->post("model");
$sys->notes = $_request->post("notes");
$sys->owner = get_user()->id;
$sys->save();
return redirect("/system/" . $sys->id);
}
public static function system_add_doc($id, $doc) {
$sys = new System($id);
if ($sys->owner == get_user()->id) {
$exist = SystemDoc::find([
["system", "=", $sys->id],
["document", "=", $doc]
])->first();
if (!$exist) {
$sd = new SystemDoc;
$sd->system = $sys->id;
$sd->document = $doc;
$sd->save();
}
return redirect("/system/" . $id);
}
return [403, "Forbidden"];
}
public static function system_del_doc($id, $doc) {
$sys = new System($id);
if ($sys->owner == get_user()->id) {
$sd = SystemDoc::find([
["system", "=", $sys->id],
["document", "=", $doc]
])->first();
if ($sd) {
$sd->delete();
}
return redirect("/system/" . $id);
}
return [403, "Forbidden"];
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
class DownloadJob extends Job {
public static function jobs() { return 3; }
public $from = null;
public $to = null;
private $_pct = 0;
public function __construct($from) {
$this->from = $from;
parent::__construct("download");
}
public function generate_to() {
$filename = ROOT . "/download/file-" . time() . "-" . rand() . ".pdf";
return $filename;
}
public function run() {
$this->to = $this->generate_to();
$ch = curl_init();
$this->status("Downloading: 0%");
$fd = fopen($this->to, "w");
curl_setopt($ch, CURLOPT_URL, $this->from);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_PRIVATE, $this);
curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
curl_setopt($ch, CURLOPT_FILETIME, true);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, [$this, 'download_progress']);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_FILE, $fd);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
print("Running...\n");
$r = curl_exec($ch);
fclose($fd);
$file = new File($this->to);
$sha256 = $file->hash();
$rev = Revision::find([["sha256", "=", $sha256]])->first();
if ($rev) {
$this->status("Duplicate file");
$this->fail();
print("Finished (duplicate)\n");
return;
}
$u = new Upload();
$u->filename = basename($this->from);
$u->sha256 = $sha256;
$u->source = $this->from;
$u->ocr = "N";
$u->ai = "N";
$u->owner = $this->getOwner();
$u->save();
$file->rename(ROOT . "/uploads/" . $u->id . ".pdf");
$j = new ImportGeminiJob($u->id);
$j->queue($u->owner);
$j = new ImportOCRJob($u->id);
$j->queue($u->owner);
$this->status("Finished");
print("Finished\n");
$this->finish();
}
function download_progress($ch, $download_size, $downloaded, $upload_size, $uploaded) {
if ($download_size == 0) return;
$pct = round($downloaded / $download_size * 100);
if ($pct != $this->_pct) {
$this->status("Downloading: " . $pct . "%");
$this->_pct = $pct;
}
}
}
+211
View File
@@ -0,0 +1,211 @@
<?php
class GeminiJob extends Job {
public static function jobs() { return 1; }
public $docid;
public $move=false;
public function __construct($docid, $source="unknown") {
parent::__construct($source);
$this->docid = $docid;
}
public function run() {
$pdf = false;
$subs = [
"Installation and Operating Information",
"Installation and Configuration",
"Installing and Getting Started",
"Installation/Operator's Manual",
"Installation/Operator's Guide",
"Programmer's Reference Guide",
"Field Maintenance Print Set",
"Illustrated Parts Breakdown",
"Installation/Owner's Guide",
"Installation Information",
"Installation/User Guide",
"User Documentation Kit",
"Programmer Information",
"Technical Description",
"Operator Information",
"Configuration Guide",
"Upgrade Information",
"Installation Manual",
"Service Information",
"Installation Guide",
"Programming Manual",
"Maintenance Manual",
"Maintenance Guide",
"Technical Summary",
"Operator's Guide",
"System Reference",
"User Information",
"Technical Manual",
"Language Manual",
"Service Manual",
"Service Guide",
"Read Me First",
"Owner's Guide",
"Release Notes",
"Options Guide",
"Users' Manual",
"User's Manual",
"HiTest Notes",
"User's Guide",
"Design Guide",
"User Manual",
"User Guide",
];
$doc = new Document($this->docid);
$minsize = 999999999999999;
foreach ($doc->revisions as $r) {
$p = new PDF($r->path() . "/doc.pdf");
if ($p->size() < $minsize) {
$minsize = $p->size();
$rev = $r;
$pdf = $p;
}
}
if (!$pdf) {
$this->fail("No files to upload");
return;
}
$prods = [];
$db = DB::getInstance();
$q = $db->query("select id, full_path from product");
while ($r = $db->nextRecord($q)) {
$prods[] = $r->id . "," . $r->full_path;
}
if ($doc->is_incoming()) {
$this->move = true;
}
file_put_contents("/tmp/category-list.csv", implode("\n", $prods) . "\n");
$this->status("Processing document with Gemini");
try {
$gemini = new Gemini();
// $gemini->verbose = true;
$p = new GeminiParameter("title", "object", "The title of the document (note: none of these parts should include the order number)", true);
$p->add_child("main_title", "string", "The primary portion of the title, properly recapitalized to fit English grammar", true);
$p->add_child("subtitle", "string", "Any portion of the title that could be used as a subtitle, such as 'user guide' or 'maintenance guide' etc. Remove this from the main title. Should not be the order number.");
$p->add_child("subsubtitle", "string", "Any part of the title that could be extra information such as version number or language. Should not be the order number.");
$gemini->add_parameter($p);
$p = new GeminiParameter("category", "integer", "The category the document best fits in from the provided list of categories in the file category-list.csv", true);
$gemini->add_parameter($p);
$gemini->add_parameter(new GeminiParameter("order_number", "string", "The document order number, often in the format XX-YYYYY-ZZ where YYYYY is based on the product code and ZZ is the type of document. Never more than 15 characters long."));
$gemini->add_parameter(new GeminiParameter("overview", "string", "A brief one-line overview of the document content in plain text format", true));
$gemini->add_parameter(new GeminiParameter("summary", "string", "A longer summary of the document content in markdown format", true));
$gemini->upload_callback([$this, "uploadcb"]);
$gemini->process_callback([$this, "processcb"]);
$gemini->attach($pdf->basename(), $pdf);
$gemini->attach("category-list.csv", new File("/tmp/category-list.csv"));
$lines = $gemini->geminiOverview();
} catch (Exception $e) {
$this->status($e->getMessage());
if ($e->getMessage() == "The request timed out. Please try again.") {
$this->retry("timeout");
} else if ($e->getMessage() == "HTTP Error 0 requesting AI assistance") {
$this->retry("error 0");
} else if (str_starts_with($e->getMessage(), "You exceeded your current quota, please check your plan and billing details.")) {
$this->retries++;
if ($this->retries < 5) {
$this->retry("quota exceeded");
} else {
$this->fail("Too many retries");
}
} else if ($e->getMessage() == "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.") {
$this->retry("overload");
} else {
$this->fail();
}
return;
}
$data = json_decode($lines, true);
if (!$data) {
$this->fail("No response from AI");
return;
}
// Reload document since this took a while
$doc = new Document($this->docid);
if (array_key_exists("title", $data)) {
$doc->subtitle = "";
$doc->subsubtitle = "";
if (array_key_exists("main_title", $data["title"])) { $doc->title = $data['title']['main_title']; }
if (array_key_exists("subtitle", $data["title"])) { $doc->subtitle = $data['title']['subtitle']; }
if (array_key_exists("subsubtitle", $data["title"])) { $doc->subsubtitle = $data['title']['subsubtitle']; }
if ($doc->subtitle != "") {
$doc->title = str_replace(" " . $doc->subtitle, "", $doc->title);
}
}
if (array_key_exists("order_number", $data)) { $doc->internal_id = substr($data['order_number'], 0, 20); }
if (array_key_exists("summary", $data)) { $doc->overview = $data['summary']; }
if (array_key_exists("overview", $data)) { $doc->oneliner = $data['overview']; }
$doc->save();
if (array_key_exists("category", $data)) {
$cat = $data['category'];
$q = $db->query("select * from product where id=:id", ["id" => $cat]);
if ($r = $db->nextRecord($q)) {
if ($this->move) {
$q = $db->query("delete from docproduct where document=:id", ["id" => $doc->id]);
}
$edp = DocProduct::find([
["product", "=", $r->id],
["document", "=", $doc->id]
])->first();
if (!$edp) {
$dp = new DocProduct;
$dp->document = $doc->id;
$dp->product = $r->id;
$dp->save();
}
} else {
print("--- Incorrect category suggested: " . $cat . "\n");
}
}
$this->status("Finished");
$this->finish();
return;
}
public function uploadcb($percent) {
$this->status("Uploading: " . $percent . "% complete");
}
public function processcb($message) {
$this->status($message);
}
function cleanup($txt) {
$txt = str_replace("Ø", "0", $txt);
$txt = str_replace(".", " ", $txt);
return $txt;
}
}
+182
View File
@@ -0,0 +1,182 @@
<?php
class ImportGeminiJob extends Job {
public static function jobs() { return 1; }
public $upload_id;
public function __construct($upload_id) {
parent::__construct("upload:" . $upload_id);
$this->upload_id = $upload_id;
}
public function run() {
Config::refresh();
$pdf = false;
$subs = [
"Installation and Operating Information",
"Installation and Configuration",
"Installing and Getting Started",
"Installation/Operator's Manual",
"Installation/Operator's Guide",
"Programmer's Reference Guide",
"Field Maintenance Print Set",
"Illustrated Parts Breakdown",
"Installation/Owner's Guide",
"Installation Information",
"Installation/User Guide",
"User Documentation Kit",
"Programmer Information",
"Technical Description",
"Operator Information",
"Configuration Guide",
"Upgrade Information",
"Installation Manual",
"Service Information",
"Installation Guide",
"Programming Manual",
"Maintenance Manual",
"Maintenance Guide",
"Technical Summary",
"Operator's Guide",
"System Reference",
"User Information",
"Technical Manual",
"Language Manual",
"Service Manual",
"Service Guide",
"Read Me First",
"Owner's Guide",
"Release Notes",
"Options Guide",
"Users' Manual",
"User's Manual",
"HiTest Notes",
"User's Guide",
"Design Guide",
"User Manual",
"User Guide",
];
$doc = new Upload($this->upload_id);
$doc->ai = "P";
$doc->save();
$pdf = $doc->getPDF();
if (!$pdf) {
$this->fail();
$doc->ai = "F";
$this->status("PDF File Missing");
$doc->save();
return;
}
if ($pdf->size() > 50000000) {
$this->status("File too large");
$doc->ai = "F";
$doc->save();
$this->fail();
return;
}
$this->status("Processing " . $doc->filename . " with Gemini");
try {
$gemini = new Gemini();
$gemini->upload_callback([$this, "uploadcb"]);
$gemini->process_callback([$this, "processcb"]);
$lines = $gemini->geminiOverview($pdf);
} catch (Exception $e) {
$mess = $e->getMessage();
print($mess . "\n");
switch ($mess) {
case "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.":
case "The request timed out. Please try again.":
case "HTTP Error 0 requesting AI assistance":
$this->retry();
print("Retrying\n");
break;
default:
$this->fail();
$this->status(substr($mess, 0, 250));
$doc->overview = $mess;
$doc->ai = "F";
$doc->save();
}
return;
}
$this->status("Postprocessing returned data");
$ld = explode("\n", $lines);
if (preg_match('/^\{(.*)\}$/', trim($ld[0]), $m)) {
$title = trim($m[1]);
$title = str_replace("", "'", $title);
$newsub = "";
$newsubsub = "";
$b = explode(":", $title);
if (count($b) > 1) {
$title = trim(array_shift($b));
$newsub = trim(array_shift($b));
$newsubsub = implode(": ", $b);
}
if ($newsub == "") {
foreach ($subs as $sub) {
if (str_ends_with($title, $sub)) {
$newsub = $sub;
$title = substr($title, 0, 0 - (strlen($sub) + 1));
break;
}
}
}
$doc->title = $title;
$doc->subtitle = $newsub;
$doc->subsubtitle = $newsubsub;
array_shift($ld);
if (trim($ld[0]) == "") {
array_shift($ld);
}
}
if (preg_match('/^\[(.*)\]$/', trim($ld[0]), $m)) {
$iid = IDMatch::find_docid($this->cleanup($m[1]));
if ($iid) {
$doc->ai_docid = $iid[0];
}
array_shift($ld);
}
$doc->overview = implode("\n", $ld);
$doc->ai = "Y";
$doc->save();
$this->status("Finished");
$this->finish();
return;
}
public function uploadcb($percent) {
$this->status("Uploading: " . $percent . "% complete");
}
public function processcb($message) {
$this->status($message);
}
function cleanup($txt) {
$txt = str_replace("Ø", "0", $txt);
$txt = str_replace(".", " ", $txt);
return $txt;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
class ImportOCRJob extends Job {
public static function jobs() { return 1; }
public $upload_id;
public function __construct($upload_id) {
parent::__construct("upload:" . $upload_id);
$this->upload_id = $upload_id;
}
public function run() {
$pdf = false;
$doc = new Upload($this->upload_id);
$doc->ocr = "P";
$doc->save();
$pdf = $doc->getPDF();
if (!$pdf) {
$this->fail();
$doc->ocr = "F";
$this->status("PDF File Missing");
$doc->save();
return;
}
$this->status("Processing " . $doc->filename . " with OCR");
$body = $pdf->text()->get_text();
if ($body == "") {
$this->status("Running OCR on PDF file");
$ocr = new PDF($pdf->path() . "-ocr.pdf");
$pdf->ocr($ocr);
$ocr->rename($pdf->path(), true);
$pdf = $doc->getPDF();
$body = $pdf->text()->get_text();
}
$doc->body = $body;
$docid = IDMatch::find_docid($doc->filename . " " . $body);
if ($docid) {
$doc->ocr_docid = $docid[0];
}
$doc->ocr = "Y";
$doc->save();
$this->status("Finished");
$this->finish();
return;
}
public function uploadcb($percent) {
$this->status("Uploading: " . $percent . "% complete");
}
public function processcb($message) {
$this->status($message);
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
class IndexJob extends Job {
public static function jobs() { return 5; }
public $revision;
public function __construct($rev) {
parent::__construct("revision:" . $rev);
$this->revision = $rev;
}
public function run() {
$db = DB::getInstance();
$rev = new Revision($this->revision);
$rev->load("body");
$body = $rev->body;
if ($body) {
$body->counted = 'P';
$body->save();
$db->query("DELETE FROM word_index WHERE revision=:rev", ["rev" => $rev->id]);
$words = $body->count_words();
foreach ($words as $word=>$hits) {
$db->query("INSERT INTO word_index (revision, word, hits) VALUES (:rev, :word, :hits)", [
"rev" => $rev->id,
"word" => substr($word, 0, 20),
"hits" => $hits
]);
}
$body->counted = 'Y';
$body->save();
$this->finish();
} else {
$this->fail();
$this->status("No body to count");
}
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
class OCRJob extends Job {
public static function jobs() { return 1; }
public $revision;
public function __construct($rev) {
parent::__construct("revision:" . $rev);
$this->revision = $rev;
}
public function run() {
$ocr = OCR::find([["revision", "=", $this->revision]])->first();
if (!$ocr) {
$ocr = new OCR;
$ocr->revision = $this->revision;
$ocr->save();
}
$this->status("Processing");
$rev = new Revision($this->revision);
$pdf = $rev->getPDF();
$text = $pdf->text()->get_text();
print("[" . $text . "]\n");
if ($text == "") {
$this->status("Running OCR on PDF");
$newpdf = new PDF($pdf->parent() . "/ocr.pdf");
$pdf->ocr($newpdf);
$text = $newpdf->text()->get_text();
}
$ocr->body = $text;
$ocr->save();
$j = new IndexJob($this->revision);
$j->queue();
$this->status("Complete");
$this->finish();
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
class ProcessJob extends Job {
public static function jobs() { return 1; }
public $docid = 0;
public function __construct($docid) {
parent::__construct("document:$docid");
$this->docid = $docid;
}
public function run() {
return false;
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
class RecompressJob extends Job {
public static function jobs() { return 1; }
public $RevisionID;
public function __construct($id) {
parent::__construct("revision:" . $id);
$this->RevisionID = $id;
}
public function run() {
$this->status("Recompressing");
$rev = new Revision($this->RevisionID);
$crev = new Revision;
$crev->revid = $rev->revid;
$crev->save();
$from = $rev->getPDF();
$to = $crev->getPDF();
$to->parent()->mkdir();
$from->recompress($to->path());
$crev->document = $rev->document;
$crev->info = $to->info();
$crev->sha256 = $to->hash();
$crev->save();
$this->status("New revision " . $crev->id);
$this->finish();
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
class RedownloadJob extends Job {
public static function jobs() { return 1; }
public $from = null;
public $to = null;
public $revid = null;
private $_pct = 0;
public function __construct($revid, $from, $to) {
$this->from = $from;
$this->to = $to;
$this->revid = $revid;
parent::__construct("revision:" . $revid);
}
public function run() {
$ch = curl_init();
$this->status("Downloading: 0%");
$fd = fopen($this->to, "w");
curl_setopt($ch, CURLOPT_URL, $this->from);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36");
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_PRIVATE, $this);
curl_setopt($ch, CURLOPT_TIMEOUT, 3600);
curl_setopt($ch, CURLOPT_FILETIME, true);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, [$this, 'download_progress']);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_FILE, $fd);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
print("Running...\n");
$r = curl_exec($ch);
fclose($fd);
$sha = hash_file("sha256", $this->to);
$rev = new Revision($this->revid);
$rev->sha256 = $sha;
$rev->save();
print("Finished\n");
$this->finish();
return;
}
function download_progress($ch, $download_size, $downloaded, $upload_size, $uploaded) {
if ($download_size == 0) return;
$pct = round($downloaded / $download_size * 100);
if ($pct != $this->_pct) {
$this->status("Downloading: " . $pct . "%");
$this->_pct = $pct;
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
class DocMeta extends Model {
protected $_classes = [
"document" => "Document",
"metadata" => "MetaType"
];
}
+17
View File
@@ -0,0 +1,17 @@
<?php
class DocProduct extends Model {
protected $_classes = [
"document" => "Document",
"product" => "Product"
];
protected $_model = [
"id" => [MODEL_SERIAL],
"created" => [MODEL_BIGINT, 11],
"updated" => [MODEL_BIGINT, 11],
"document" => [MODEL_OBJECT, "Document"],
"product" => [MODEL_OBJECT, "Product"]
];
}
+209
View File
@@ -0,0 +1,209 @@
<?php
class Document extends Model {
protected $_computed = [
"related" => "get_related",
"revisions" => "get_revisions",
"products" => "get_products",
"metadata" => "get_metadata",
"attachments" => "get_attachments",
];
public function get_revisions() {
return Revision::find([["document", "=", $this->id]])->orderByDesc("rating")->all();
}
public function get_related() {
$revs = Revision::find([["document", "=", $this->id]])->all();
$related = new Collection;
foreach ($revs as $rev) {
$q = DB::getInstance()->query("
select distinct
id,
internal_id,
title,
subtitle,
subsubtitle
from
document
where
internal_id in (
select
distinct words.word
from
words,
revwords
where
words.id=revwords.word
and
revwords.revision=:rev
and
words.word like '%-%-%'
)", array("rev" => $rev->id));
while ($r = DB::getInstance()->nextRecord($q)) {
if ($r->id != $this->id) {
$related->push(new Document($r->id));
}
}
}
return $related;
}
public function overview_md() {
return \Michelf\Markdown::defaultTransform($this->overview);
}
public function on_delete() {
DB::getInstance()->query("delete from docproduct where document=:id", ["id" => $this->id]);
}
private $_products = null;
public function get_products() {
if ($this->_products == null) {
$this->_products = new Collection;
$dpl = DocProduct::find([["document", "=", $this->id]])->all();
foreach ($dpl as $dp) {
$dp->load("product");
$this->_products->push($dp->product);
}
}
return $this->_products;
}
public function duplicate() {
$newdoc = new Document();
$newdoc->title = $this->title;
$newdoc->subtitle = $this->subtitle;
$newdoc->subsubtitle = $this->subsubtitle;
$newdoc->overview = $this->overview;
$newdoc->internal_id = $this->internal_id;
$newdoc->owner = $this->owner;
$newdoc->year = $this->year;
$newdoc->month = $this->month;
$newdoc->save();
$dpl = DocProduct::find([["document", "=", $this->id]])->all();
foreach ($dpl as $dp) {
$ndp = new DocProduct();
$ndp->document = $newdoc->id;
$ndp->product = $dp->product;
$ndp->save();
}
return $newdoc;
}
public function remove_product($id) {
$dp = DocProduct::find([["document", "=", $this->id], ["product", "=", $id]])->first();
if ($dp) {
$dp->delete();
}
}
public function set_metadata($metadata, $value) {
if (!$metadata) return false;
$m = DocMeta::find([["document", "=", $this->id], ["metadata", "=", $metadata]])->first();
if (!$m) {
$m = new DocMeta();
$m->document = $this->id;
$m->metadata = $metadata;
}
$m->data = $value;
$m->save();
return $m->id;
}
public function get_metadata() {
return DocMeta::find([["document", "=", $this->id]])->all();
}
public function remove_metadata($metadata) {
$m = DocMeta::find([["document", "=", $this->id], ["metadata", "=", $metadata]])->first();
if ($m) {
$m->delete();
}
}
public function get_metadata_by_id($metadata) {
$m = DocMeta::find([["document", "=", $this->id], ["metadata", "=", $metadata]])->first();
if (!$m) {
return "";
}
return $m->data;
}
public function guess_docid() {
$text = $this->title . " " . $this->subtitle . " " . $this->subsubtitle;
$text .= $this->overview;
$words = $this->get_words($text);
return IDMatch::find_docid($words)[0];
}
function get_words($text) {
$text = str_replace("\r", " ", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n", " ", $text);
$text = str_replace("\"", "", $text);
$text = str_replace(",", " ", $text);
$text = str_replace("", "-", $text);
$text = str_replace("~", "-", $text);
$text = str_replace("--", "-", $text);
$text = preg_replace('/([^a-zA-Z\-0-9]+)/', ' ', $text);
//$text = preg_replace("/([A-Za-z])(- )/", '$1', $text);
$words = preg_split('/\s+/', $text);
return $words;
}
function get_attachments() {
$ap = ROOT . "/attachments/" . $this->id;
$files = new Collection;
if (!file_exists($ap)) {
return $files;
}
if (!is_dir($ap)) {
return $files;
}
$dir = opendir($ap);
while ($f = readdir($dir)) {
if (substr($f, 0, 1) == ".") continue;
$f = new File($ap . "/" . $f);
$files->push($f);
}
return $files;
}
public static function find_by_docid($docid) {
$db = DB::getInstance();
$q = $db->query("select id from document where internal_id=:docid", ["docid" => $docid]);
if ($r = $db->nextRecord($q)) {
return new Document($r->id);
}
return false;
}
public function is_incoming() {
foreach ($this->get_products() as $prod) {
if (preg_match("/ Incoming /", $prod->full_path, $m)) {
return true;
}
}
return false;
}
}
+4
View File
@@ -0,0 +1,4 @@
<?php
class Favourite extends Model {
}
+51
View File
@@ -0,0 +1,51 @@
<?php
class IDMatch extends Model {
public static function find_docid($words) {
if (!is_array($words)) {
$words = IDMatch::get_words($words);
}
$matches = IDMatch::find()->orderBy("weight")->all();
foreach ($matches as $match) {
$preg = '/^' . $match->regex . '$/';
foreach ($words as $word) {
$word = strtoupper($word);
$word = str_replace("(", "C", $word);
$word = str_replace("=", "-", $word);
$word = str_replace("--", "-", $word);
if (preg_match($preg, $word, $m)) {
return [$m[1], @$m[2]];
}
}
}
return false;
}
public static function get_words($text) {
$text = str_replace("\r", " ", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n\n", "\n", $text);
$text = str_replace("\n", " ", $text);
$text = str_replace("\"", "", $text);
$text = str_replace(",", " ", $text);
$text = str_replace("", "-", $text);
$text = str_replace("", "-", $text);
$text = str_replace("~", "-", $text);
$text = str_replace("--", "-", $text);
$text = preg_replace('/([^a-zA-Z\-0-9]+)/', ' ', $text);
//$text = preg_replace("/([A-Za-z])(- )/", '$1', $text);
$words = preg_split('/\s+/', $text);
return $words;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
class MetaType extends Model {
protected $table = "metatypes";
public static function name($id) {
$m = new MetaType($id);
return $m->name;
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
class OCR extends Model {
public function get_words() {
$t = new Text($this->body);
return $t->words();
}
public function count_words() {
$words = $this->get_words();
$counted = [];
foreach ($words as $word) {
if (strlen($word) > 2) {
@$counted[strtoupper($word)] ++;
}
}
return $counted;
}
}
+163
View File
@@ -0,0 +1,163 @@
<?php
class Product extends Model {
protected $_classes = [
"parent" => "Product"
];
protected $_computed = [
"documents" => "get_documents",
"children" => "get_children",
];
protected $_triggers = [
"parent" => "update_path",
"title" => "update_path",
];
private $_children = null;
public function get_children() {
$c = $this->cache_get("children");
if ($c) return $c;
if ($this->_children == null) {
$this->_children = Product::find([["parent", "=", $this->id]])->orderBy("title")->all();
}
$this->cache_set("children", $this->_children);
return $this->_children;
}
public function get_full_title() {
$tree = $this->get_tree();
$n = [];
foreach ($tree as $t) {
$n[] = $t->title;
}
$out = implode(" / ", $n);
return $out;
}
public function update_path($ppath = null) {
if ($ppath == null) {
$this->full_path = $this->get_full_title();
} else {
$this->full_path = $ppath . " / " . $this->title;
}
$this->full_path = str_replace("/ / ", "/ ", $this->full_path);
$this->save();
foreach ($this->get_children() as $child) {
$child->update_path($this->fill_path);
}
}
public function get_tree() {
$out = [];
if ($this->load("parent")) {
$p = $this->parent;
$out = $p->get_tree();
}
array_push($out, $this);
return $out;
}
public function overview_md() {
return \Michelf\Markdown::defaultTransform($this->overview);
}
public function on_delete() {
DB::getInstance()->query("delete from docproduct where product=:id", ["id" => $this->id]);
}
private $_documents = null;
public function get_documents() {
$d = $this->cache_get("documents");
if ($d) return $d;
if ($this->_documents == null) {
//$dpl = DocProduct::find([["product", "=", $this->id]])->limit(100)->all();
$dpl = DocProduct::find([["product", "=", $this->id]])->all();
$this->_documents = new Collection;
foreach ($dpl as $dp) {
if ($dp->load("document")) {
$this->_documents->push($dp->document);
}
}
$this->_documents->sort("subsubtitle", true);
$this->_documents->sort("subtitle", true);
$this->_documents->sort("title", true);
}
$this->cache_set("documents", $this->_documents);
return $this->_documents;
}
public function add_document($doc) {
$dp = new DocProduct;
$dp->product = $this->id;
$dp->document = $doc->id;
$dp->save();
}
public function meta() {
if ($this->metadata == null) {
return [];
}
return explode(",", $this->metadata);
}
public function add_meta($id, $save = true) {
$m = $this->meta();
if (!in_array($id, $m)) {
$m[] = $id;
}
$this->metadata = implode(",", $m);
if ($save) $this->save();
}
public function del_meta($id, $save = true) {
$m = $this->meta();
$o = [];
foreach ($m as $v) {
if ($v != $id) {
$o[] = $v;
}
}
$this->metadaya = implode(",", $o);
if ($save) $this->save();
}
public function documents_sorted_by_meta() {
if ($this->metadata == null) {
$meta = [];
} else {
$meta = explode(",", $this->metadata);
}
$docs = $this->get_documents();
while (count($meta) > 0) {
$mid = array_pop($meta);
$docs->sort_with_function( function($a, $b) use ($mid) {
$va = $a->get_metadata_by_id($mid);
$vb = $b->get_metadata_by_id($mid);
if ($va > $vb) return 1;
if ($va < $vb) return -1;
return 0;
});
}
return $docs;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
class Revision extends Model {
protected $_classes = [
"document" => "Document"
];
protected $_computed = [
"body" => "get_body",
];
protected $transform = [
"info" => "json"
];
function path() {
$rs = sprintf("%011d", $this->id);
$d1 = substr($rs, 0, 2);
$d2 = substr($rs, 2, 3);
$d3 = substr($rs, 5, 3);
$d4 = substr($rs, 8, 3);
$p = sprintf("%s/pdf/%s/%s/%s/%s", ROOT, $d1, $d2, $d3, $d4);
return $p;
}
function filename() {
if ($this->document) {
if (is_numeric($this->document)) {
$this->load("document");
}
$out = $this->document->internal_id;
if (($this->revno != "") && ($this->revno != "0")) {
$out .= "-";
$out .= $this->revno;
}
$out .= " ";
$out .= $this->document->title . " " . $this->document->subtitle . " " . $this->document->subsubtitle;
} else {
$out = "doc";
}
$out = trim($out);
$out.= ".pdf";
$out = str_replace(" ", "_", $out);
$out = str_replace("/", "_", $out);
return $out;
}
function create_cover() {
$p = $this->path();
$pdf = new PDF($p . "/doc.pdf");
$cover = $p . "/cover.jpg";
$pdf->extract_page(0, $cover);
}
function cover($size = null) {
$f = new File($this->path() . "/cover.jpg");
if (!$f->exists()) {
$this->create_cover();
}
if ($size == null) {
return "/cover/" . $this->id . "/cover.jpg";
}
return "/cover/" . $this->id . "/" . $size . "/cover.jpg";
}
private $_body = null;
public function get_body() {
if ($this->_body == null) {
$ocr = OCR::find([["revision", "=", $this->id]])->first();
if (!$ocr) {
return false;
}
$this->_body = $ocr;
}
return $this->_body;
}
public function get_page($page, $dpi = 300) {
$file = new File(sprintf("%s/pages/%04d-%d.jpg", $this->path(), $page, $dpi));
if ($file->exists()) {
return new Image($file);
}
$dir = sprintf("%s/pages", $this->path());
if (!file_exists($dir)) {
mkdir($dir, 0777);
}
$pdf = new PDF(sprintf("%s/doc.pdf", $this->path()));
$img = $pdf->extract_page($page, $file->path(), $dpi);
if ($img) {
return $img;
}
return null;
}
public function purge() {
$dir = opendir($this->path());
while ($file = readdir($dir)) {
if (str_ends_with($file, ".jpg")) {
unlink($this->path() . "/" . $file);
}
}
closedir($dir);
$dir = opendir($this->path() . "/pages");
while ($file = readdir($dir)) {
if (str_ends_with($file, ".jpg")) {
unlink($this->path() . "/pages/" . $file);
}
}
closedir($dir);
}
public function getPDF() {
$p = $this->path();
$pdf = new PDF($p . "/doc.pdf");
return $pdf;
}
}
+4
View File
@@ -0,0 +1,4 @@
<?php
class Spider extends Model {
}
+4
View File
@@ -0,0 +1,4 @@
<?php
class SpiderBanned extends Model {
}
+4
View File
@@ -0,0 +1,4 @@
<?php
class SpiderDom extends Model {
}
+5
View File
@@ -0,0 +1,5 @@
<?php
class SpiderPage extends Model {
protected $table="pages";
}
+23
View File
@@ -0,0 +1,23 @@
<?php
class System extends Model {
protected $table = "systems";
protected $_computed = [
"documents" => "get_documents",
];
public function get_documents() {
$docs = SystemDoc::find([["system", "=", $this->id]])->all();
$dl = new Collection;
foreach ($docs as $d) {
$doc = new Document($d->document);
if ($doc) {
$dl->add($doc);
}
}
$dl->sort("title");
return $dl;
}
}
+5
View File
@@ -0,0 +1,5 @@
<?php
class SystemDoc extends Model {
protected $table = "systemdocs";
}
+8
View File
@@ -0,0 +1,8 @@
<?php
class Upload extends Model {
public function getPDF() {
return new PDF(ROOT . "/uploads/" . $this->id . ".pdf");
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
class User extends Model {
public static $table = "users";
public function is_favourite($doc) {
$f = Favourite::find([
["owner", "=", $this->id],
["document", "=", $doc]])->first();
if ($f) {
return true;
}
return false;
}
public function set_favourite($doc) {
$f = Favourite::find([
["owner", "=", $this->id],
["document", "=", $doc]])->first();
if (!$f) {
$f = new Favourite;
$f->owner = $this->id;
$f->document = $doc;
$f->save();
}
}
public function unset_favourite($doc) {
$f = Favourite::find([
["owner", "=", $this->id],
["document", "=", $doc]])->first();
if ($f) {
$f->delete();
}
}
public function favourites() {
$f = Favourite::find([
["owner", "=", $this->id],
])->all();
return $f;
}
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env php
<?php
$opts = getopt("c:j:");
$class = null;
$jobid = null;
if (array_key_exists("c", $opts)) {
$class = $opts['c'];
}
if (array_key_exists("j", $opts)) {
$jobid = $opts['j'];
}
require_once(__DIR__ . "/../app.php");
print("Job Runner Starting\n");
$running = true;
while ($running) {
$job = Job::consumeNextJob($class);
if ($job) {
print("Executing " . $job->getJobClass() . " job " . $job->jobID() . "\n");
try {
$job->run();
} catch (PDOException $e) {
print_r($e);
} catch (Exception $e) {
print_r($e);
}
print("Job finished\n");
} else {
sleep(1);
}
if (file_exists("/tmp/dpr_jobrunner_quit")) {
unlink("/tmp/dpr_jobrunner_quit");
$running = false;
}
}
print("Job Runner Exited Cleanly\n");
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env php
<?php
file_put_contents("/tmp/dpr_jobrunner_quit", "");
Executable
+7
View File
@@ -0,0 +1,7 @@
{
"require": {
"michelf/php-markdown": "^2.0",
"jenssegers/blade": "^2.0",
"illuminate/view": "11.7.0"
}
}
+54
View File
@@ -0,0 +1,54 @@
ARG PHP_VERSION=8.4
FROM php:${PHP_VERSION}-rc-apache-bookworm AS base
RUN set -eux; \
apt-get update && \
apt-get install -y --no-install-recommends \
libmemcached-dev \
libssl-dev \
libzip-dev \
libzip4 \
libjpeg-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
ocrmypdf \
poppler-utils \
mariadb-client \
libpng-dev \
tmux \
imagemagick \
pdftk \
&& \
printf "" | pecl install -D 'with-libmemcached-dir="/usr"' memcached && \
docker-php-ext-enable memcached && \
rm -rf /var/lib/apt/lists/*
COPY policy.xml /etc/ImageMagick-6
RUN apt-get update \
&& docker-php-ext-install mysqli pdo pdo_mysql \
&& docker-php-ext-enable pdo_mysql
RUN apt-get update \
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install gd \
&& docker-php-ext-enable gd
RUN apt-get update \
&& docker-php-ext-configure pcntl --enable-pcntl \
&& docker-php-ext-install pcntl \
&& docker-php-ext-enable pcntl
RUN a2enmod rewrite
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf
RUN sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
COPY memory.ini /usr/local/etc/php/conf.d
RUN apt-get update \
&& apt-get install -y internetarchive
RUN rm -f /var/log/apache2/*
Executable
+23
View File
@@ -0,0 +1,23 @@
import time
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return f'Hello World! I have been seen {count} times.\n'
Executable
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
echo "Booting..."
apachectl start
while `true`; do
sleep 1
done
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
pacman -Sy
pacman -S --noconfirm --overwrite=/usr/lib/* glibc gcc-libs openssl-1.1
ldconfig
pacman -S --noconfirm apache mariadb-clients php php-gd php-memcache
ls -l /usr/lib/libcrypto.so*
pacman -S --needed --noconfirm sudo # Install sudo
useradd builduser -m # Create the builduser
passwd -d builduser # Delete the buildusers password
printf 'builduser ALL=(ALL) ALL\n' | tee -a /etc/sudoers # Allow the builduser passwordless sudo
sudo -u builduser bash -c 'cd /tmp && git clone https://aur.archlinux.org/ocrmypdf ocrmypdf && cd ocrmypdf && makepkg -si --noconfirm' # Clone and build a package
+30
View File
@@ -0,0 +1,30 @@
services:
web:
build: .
restart: always
ports:
- "8000:80"
runtime: nvidia
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- type: bind
source: /data/Web/decpdf.site/
target: /var/www/html/
db:
image: "mariadb"
restart: always
volumes:
- type: bind
source: /data/Web/decpdf.site/database
target: /var/lib/mysql
environment:
MARIADB_ROOT_PASSWORD: wibble
memcache:
image: "memcached"
restart: always
+3
View File
@@ -0,0 +1,3 @@
memory_limit = 2G
upload_max_filesize = 2G
post_max_size = 2G
+98
View File
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
<!ELEMENT policymap (policy)*>
<!ATTLIST policymap xmlns CDATA #FIXED ''>
<!ELEMENT policy EMPTY>
<!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
]>
<!--
Configure ImageMagick policies.
Domains include system, delegate, coder, filter, path, or resource.
Rights include none, read, write, execute and all. Use | to combine them,
for example: "read | write" to permit read from, or write to, a path.
Use a glob expression as a pattern.
Suppose we do not want users to process MPEG video images:
<policy domain="delegate" rights="none" pattern="mpeg:decode" />
Here we do not want users reading images from HTTP:
<policy domain="coder" rights="none" pattern="HTTP" />
The /repository file system is restricted to read only. We use a glob
expression to match all paths that start with /repository:
<policy domain="path" rights="read" pattern="/repository/*" />
Lets prevent users from executing any image filters:
<policy domain="filter" rights="none" pattern="*" />
Any large image is cached to disk rather than memory:
<policy domain="resource" name="area" value="1GP"/>
Use the default system font unless overwridden by the application:
<policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>
Define arguments for the memory, map, area, width, height and disk resources
with SI prefixes (.e.g 100MB). In addition, resource policies are maximums
for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
exceeds policy maximum so memory limit is 1GB).
Rules are processed in order. Here we want to restrict ImageMagick to only
read or write a small subset of proven web-safe image types:
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
-->
<policymap>
<!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
<policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="map" value="512MiB"/>
<policy domain="resource" name="width" value="16KP"/>
<policy domain="resource" name="height" value="16KP"/>
<!-- <policy domain="resource" name="list-length" value="128"/> -->
<policy domain="resource" name="area" value="128MP"/>
<policy domain="resource" name="disk" value="1GiB"/>
<!-- <policy domain="resource" name="file" value="768"/> -->
<!-- <policy domain="resource" name="thread" value="4"/> -->
<!-- <policy domain="resource" name="throttle" value="0"/> -->
<!-- <policy domain="resource" name="time" value="3600"/> -->
<!-- <policy domain="coder" rights="none" pattern="MVG" /> -->
<!-- <policy domain="module" rights="none" pattern="{PS,PDF,XPS}" /> -->
<!-- <policy domain="path" rights="none" pattern="@*" /> -->
<!-- <policy domain="cache" name="memory-map" value="anonymous"/> -->
<!-- <policy domain="cache" name="synchronize" value="True"/> -->
<!-- <policy domain="cache" name="shared-secret" value="passphrase" stealth="true"/>
<!-- <policy domain="system" name="max-memory-request" value="256MiB"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- <policy domain="system" name="font" value="/path/to/font.ttf"/> -->
<!-- <policy domain="system" name="pixel-cache-memory" value="anonymous"/> -->
<!-- <policy domain="system" name="shred" value="2"/> -->
<!-- <policy domain="system" name="precision" value="6"/> -->
<!-- not needed due to the need to use explicitly by mvg: -->
<!-- <policy domain="delegate" rights="none" pattern="MVG" /> -->
<!-- use curl -->
<policy domain="delegate" rights="none" pattern="URL" />
<policy domain="delegate" rights="none" pattern="HTTPS" />
<policy domain="delegate" rights="none" pattern="HTTP" />
<!-- in order to avoid to get image with password text -->
<policy domain="path" rights="none" pattern="@*"/>
<!-- disable ghostscript format types -->
<policy domain="coder" rights="none" pattern="PS" />
<policy domain="coder" rights="none" pattern="PS2" />
<policy domain="coder" rights="none" pattern="PS3" />
<policy domain="coder" rights="none" pattern="EPS" />
<policy domain="coder" rights="none" pattern="XPS" />
</policymap>
+15
View File
@@ -0,0 +1,15 @@
URL=https://decpdf.site
DEBUG=true
DB_HOST=host
DB_USER=user
DB_PASS=pass
DB_DATABASE=db
GEMINI_KEY=["3fefyhwp98eoyepoytpotywpertferyrtyueoru", "flcs8yli8ty89weylicu4ynkeuhgkdshei8rhcn"]
GEMINI_MODEL=gemini-3.1-flash-lite
MAGICK=convert
+12
View File
@@ -0,0 +1,12 @@
<?php
class AntiSpam {
public static function tarpit() {
while (true) {
print(chr(rand() & 0xff));
ob_flush();
flush();
sleep(1);
}
}
}
Executable
+77
View File
@@ -0,0 +1,77 @@
<?php
class App {
static function dispatch($req) {
$apiroute = Routes::find_api($req->type(), $req->path());
if ($apiroute !== false) { // It is an API route. Treat it as such.
$req->set_route($apiroute);
$out = $apiroute->call($req);
header("Content-type: application/json");
if ($out instanceof Collection) {
print(json_encode($out->all()));
return;
}
if (is_array($out)) { // This is an array return type. Do things depending on the result key
$status = $out[0];
$data = $out[1];
if (count($out) > 2) {
$headers = $out[2];
} else {
$headers = [];
}
header("HTTP/1.1 $status");
foreach ($headers as $k=>$v) {
header("$k: $v");
}
if (is_array($data)) {
print(json_encode($data));
} else {
print($data);
}
return;
}
print(json_encode($out));
return;
}
$webroute = Routes::find_web($req->type(), $req->path());
if ($webroute !== false) { // It is a WEB route. Treat it as such.
$req->set_route($webroute);
$out = $webroute->call($req);
if (is_array($out)) { // This is an array return type. Do things depending on the result key
$status = $out[0];
$data = $out[1];
if (count($out) > 2) {
$headers = $out[2];
} else {
$headers = [];
}
header("HTTP/1.1 $status");
foreach ($headers as $k=>$v) {
header("$k: $v");
}
print($data);
return;
}
if ($out instanceof File) {
$out->set_header("Content-Length", $out->size());
$out->emit();
return;
}
print($out);
return;
}
header("HTTP/1.1 404 Not Found");
print(blade("404"));
}
}
Executable
+183
View File
@@ -0,0 +1,183 @@
<?php
class Auth {
static function routes() {
Routes::add_web("GET", "/register", ["Auth", "register"]);
Routes::add_web("POST", "/register", ["Auth", "do_register"]);
Routes::add_web("GET", "/login", ["Auth", "login"]);
Routes::add_web("POST", "/login", ["Auth", "do_login"]);
Routes::add_web("GET", "/logout", ["Auth", "logout"]);
Routes::add_web("GET", "/account", ["Auth", "account"], ["Auth", "logged_in"]);
Routes::add_web("POST", "/account/chpass", ["Auth", "chpass"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/account/{tab}", ["Auth", "account"], ["Auth", "logged_in"]);
}
static function chpass($_request) {
$session = Session::all_data();
$user = get_user();
$current = $_request->post("current");
$chash = hash("sha256", $current);
if ($chash != $user->password) {
flash("error", "Wrong password. Try again.");
return redirect("/account");
}
$p1 = $_request->post("pass1");
$p2 = $_request->post("pass2");
if ($p1 != $p2) {
flash("error", "Your new passwords don't match.");
return redirect("/account");
}
if (strlen($p1) < 6) {
flash("error", "Your new password is too short. Pick one that is 6 characters or more.");
return redirect("/account");
}
$user->password = hash("sha256", $p1);
$user->save();
flash("success", "Your password has been changed.");
return redirect("/account");
}
static function account($tab = "account") {
$session = Session::all_data();
return blade("account", ["tab" => $tab, "session" => $session, "user" => get_user()]);
}
static function can_upload() {
$user = get_user();
if (!$user) return false;
return $user->can_upload == "Y";
}
static function can_moderate() {
$user = get_user();
if (!$user) return false;
return $user->can_moderate == "Y";
}
static function register() {
if (array_key_exists("HTTP_REFERER", $_SERVER)) {
Session::set("login_return", $_SERVER['HTTP_REFERER']);
} else {
Session::set("login_return", "/");
}
return blade("register", ["username" => "", "email" => "", "password" => "", "confirm" => ""]);
}
static function do_register($_request) {
$username = $_request->post("username");
$email = $_request->post("email");
$password = $_request->post("password");
$confirm = $_request->post("confirm");
if ($username == "") {
flash("error", "You haven't provided a username.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if (!preg_match("#^[a-zA-Z0-9_\-]+$#", $username)) {
flash("error", "Username contains invalid characters. Use letters, numbers, _ and - only.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if (strlen($username) > 20) {
flash("error", "Username is too long. Keep it under 20 characters.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if ($email == "") {
flash("error", "You haven't provided an email address.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if (strlen($password) < 6) {
flash("error", "You need to give a password of at least 6 characters.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if ($password != $confirm) {
flash("error", "Your two passwords do not match.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
flash("error", "Your email address doesn't appear to be valid.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
$exist = User::find([["username", "=", strtolower($username)]])->first();
if ($exist) {
flash("error", "That username has already been taken. Pick another.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
$exist = User::find([["email", "=", $email]])->first();
if ($exist) {
flash("error", "An account with that email address already exists.");
return blade("register", ["username" => $username, "email" => $email, "password" => $password, "confirm" => $confirm]);
}
$u = new User;
$u->username = $username;
$u->email = $email;
$u->password = hash("sha256", $password);
$u->save();
Session::set("user", $u->id);
flash("success", "Registration successful. You have also been automatically logged in.");
$ret = Session::get("login_return");
return redirect($ret);
}
static function logged_in() {
$user = get_user();
return $user !== false;
}
static function do_login() {
$username = $_POST['username'];
$password = $_POST['password'];
if (!$username) return blade("login");
if (!$password) return blade("login");
$user = User::find([["username", "=", $username]])->first();
if (!$user) {
$user = User::find([["email", "=", $username]])->first();
}
$pwe = hash("sha256", $password);
if ($pwe == $user->password) {
Session::set("user", $user->id);
flash("success", "Log in successful");
$ret = Session::get("login_return");
return redirect($ret);
}
flash("error", "Invalid username or password.");
return blade("login");
}
public static function login() {
if (array_key_exists("HTTP_REFERER", $_SERVER)) {
Session::set("login_return", $_SERVER['HTTP_REFERER']);
} else {
Session::set("login_return", "/");
}
return blade("login");
}
public static function logout() {
Session::unset("user");
return redirect($_SERVER['HTTP_REFERER']);
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
class Collection implements Countable,Iterator,ArrayAccess {
private $records;
private $position = 0;
public function __construct($data = null) {
if ($data !== null) {
$this->records = $data;
} else {
$this->records = [];
}
}
public function add($val) {
$this->records[] = $val;
}
public function first() {
if (count($this->records) == 0) return null;
return $this->records[0];
}
public function last() {
if (count($this->records) == 0) return null;
return $this->records[count($this->records)-1];
}
public function pop() {
if (count($this->records) == 0) return null;
return array_pop($this->records);
}
public function shift() {
if (count($this->records) == 0) return null;
return array_shift($this->records);
}
public function push($val) {
return array_push($this->records, $val);
}
public function unshift($val) {
return array_unshift($this->records, $val);
}
public function get($n) {
return isset($this->records[$offset]) ? $this->records[$offset] : null;
}
public function all() {
return $this->records;
}
// Countable
public function count() : int {
return count($this->records);
}
// Iterator
public function rewind() : void {
$this->position = 0;
}
public function current() : mixed {
return $this->records[$this->position];
}
public function key() : mixed {
return $this->position;
}
public function next() : void {
++$this->position;
}
public function valid() : bool {
return isset($this->records[$this->position]);
}
// ArrayAccess
public function offsetSet($offset, $value) : void {
if (is_null($offset)) {
$this->push($value);
} else {
$this->records[$offset] = $value;
}
}
public function offsetExists($offset) : bool {
return isset($this->records[$offset]);
}
public function offsetUnset($offset) : void {
unset($this->records[$offset]);
}
public function offsetGet($offset) : mixed {
return isset($this->records[$offset]) ? $this->records[$offset] : null;
}
public function each($f) {
if ($f instanceof Closure) {
foreach ($this->records as $r) {
$f->call($this, $r);
}
return;
}
if (is_array($f)) {
if (method_exists($f[0], $f[1])) {
$cl = $f[0];
$fun = $f[1];
foreach ($this->records as $r) {
$cl::$fun($r);
}
}
return;
}
}
public function sort($field, $ins=false) {
usort($this->records, function ($a, $b) use ($field,$ins) {
$aa = $a->$field;
$bb = $b->$field;
if ($ins) {
$aa = strtolower_null($aa);
$bb = strtolower_null($bb);
}
if ($aa > $bb) {
return 1;
}
if ($aa < $bb) {
return -1;
}
return 0;
});
}
public function sort_with_function($func) {
usort($this->records, $func);
}
public function range($start, $len = 0) {
if ($len == 0) {
// 0 .. $start
return array_splice($this->records, 0, $start);
}
return array_splice($this->records, $start, $len);
}
public function glob($key, $pattern) {
$nc = new Collection;
foreach ($this->records as $r) {
if (fnmatch(strtolower_null($pattern), strtolower_null($r->$key))) {
$nc->push($r);
}
}
return $nc;
}
public function merge($other) {
$o = new Collection;
foreach ($this->records as $r) {
$o->push($r);
}
foreach ($other->all() as $r) {
$o->push($r);
}
return $o;
}
public static function from_array($arr) {
$c = new Collection();
foreach ($arr as $k=>$v) {
$ob = new stdClass;
$ob->key = $k;
$ob->value = $v;
$c->pusk($ob);
}
return $c;
}
}
Executable
+32
View File
@@ -0,0 +1,32 @@
<?php
class Config {
private static $config = null;
private function __construct() {
}
public static function get(string $val, mixed $default = null) : mixed {
if (Config::$config == null) {
Config::$config = parse_ini_file(__DIR__ . "/../.env", false, INI_SCANNER_RAW);
}
if (array_key_exists($val, Config::$config)) {
$c = Config::$config[$val];
try {
$j = json_decode($c, false);
if ($j) return $j;
} catch (Exception $e) {
print_r($e);
}
return $c;
} else {
return $default;
}
}
public static function refresh() {
Config::$config = parse_ini_file(__DIR__ . "/../.env", false, INI_SCANNER_RAW);
}
}
Executable
+141
View File
@@ -0,0 +1,141 @@
<?php
class DB {
private static $instance = null;
private $db = null;
private function __construct() {
}
public static function getInstance() {
if (DB::$instance == null) {
DB::$instance = new DB;
}
return DB::$instance;
}
public function connect($dbuser, $dbpass, $dbhost, $dbname) {
$this->db = new \PDO("mysql:dbname=$dbname;host=$dbhost;charset=utf8mb4",$dbuser,$dbpass);
//$this->query("SET NAMES 'utf8' COLLATE 'utf8_general_ci'");
}
function query($query,$params = array()) {
$q = $this->db->prepare($query);
$q->execute($params);
$e = $q->errorInfo();
if($e[0]!='00000') {
print "<span class='error'>";
print $e[2];
print "</span>";
return false;
}
return $q;
}
function nextRecord($query) {
$next = $query->fetchObject();
return $next;
}
function id() {
return $this->db->lastInsertId();
}
function update($table,$id,$data) {
$values = array();
foreach($data as $k=>$v) {
$values[] = "`" . $k . "`" . "=:" . $k;
}
$query = sprintf("UPDATE `%s` set " . implode(",",$values) . " WHERE id=:id",$table);
$data['id'] = $id;
$q = $this->query($query,$data);
$id = $this->id();
return $id;
}
function insert($table,$data) {
$fields = array();
$values = array();
foreach($data as $k=>$v) {
$fields[] = $k;
$values[] = ":" . $k;
}
$query = sprintf("INSERT IGNORE INTO `%s` (" . implode(",",$fields) . ") VALUES (" . implode(",",$values) . ")",$table);
$q = $this->query($query,$data);
$id = $this->id();
return $id;
}
function set($table,$record,$field,$value) {
$this->query("UPDATE `$table` SET `$field`=:f WHERE id=:i",array(
'f'=>$value,
'i'=>$record
));
}
function select($table,$record) {
$query = sprintf("SELECT * FROM `%s` WHERE id=:id",$table);
$q = $this->query($query,array("id" => $record));
$r = $this->nextRecord($q);
return $r;
}
function getTableStructure($table) {
$query = sprintf("DESCRIBE `%s`", $table);
$q = $this->query($query);
$fields = [];
while ($r = $this->nextRecord($q)) {
$field = $r['Field'];
$type = $r['Type'];
$extra = $r['Extra'];
$fingerprint = trim("$type $extra");
$f = new stdClass;
$f->name = $field;
if (preg_match('/^(.*)\((\d+)\)(.*)$/', $fingerprint, $m)) {
$fingerprint = $m[1] . $m[3];
$f->length = $m[2];
}
$f->type == -1;
$f->unsigned = false;
switch ($fingerprint) {
case "bigint unsigned auto_increment": $f->type = MODEL_SERIAL; break;
case "text": $f->type = MODEL_TEXT; break;
case "longtext": $f->type = MODEL_TEXT; break;
case "blob": $f->type = MODEL_TEXT; break;
case "longblob": $f->type = MODEL_TEXT; break;
case "tinyint": $f->type = MODEL_TINYINT; break;
case "tinyint unsigned": $f->type = MODEL_TINYINT; $f->unsigned = true; break;
case "int": $f->type = MODEL_INT; break;
case "int unsigned": $f->type = MODEL_INT; $f->unsigned = true; break;
case "bigint": $f->type = MODEL_BIGINT; break;
case "bigint unsigned": $f->type = MODEL_BIGINT; $f->unsigned = true; break;
case "char": $f->type = MODEL_CHAR; break;
case "varchar": $f->type = MODEL_VARCHAR; break;
}
}
}
public function all($query) {
$o = new Collection;
while ($r = $this->nextRecord($query)) {
$o->push($r);
}
return $o;
}
}
+103
View File
@@ -0,0 +1,103 @@
<?php
require_once("Config.php");
class ErrorHandler {
public static function checkForFatalCrash() {
$error = error_get_last();
if ($error) {
if ($error['type'] == E_ERROR) {
ErrorHandler::handleException(new ErrorException($error['message'], 0, $error['type'], $error['file'], $error['line']));
}
}
}
public static function printErrorEntry($type, $message, $file, $line) {
?>
<?php
}
public static function getFileLines($file, $start, $end) {
$f = file_get_contents($file);
$l = explode("\n", $f);
if ($start < 0) $start = 0;
if ($end >= count($l)) $end = count($l) -1;
$out = [];
for ($i = $start; $i <= $end; $i++) {
$out[] = $l[$i];
}
return $out;
}
public static function handleException($e) {
header("Content-Type: text/html");
header("X-Error-Message: " . $e->getMessage());
header("X-Error-Line: " . $e->getLine());
header("X-Error-File: " . $e->getFile());
header("X-Error-Code: " . $e->getCode());
$trace = $e->getTrace();
array_shift($trace);
$i = 0;
foreach ($trace as $t) {
$i++;
header("X-Error-Trace-" . $i . ": " . $t['file'] . "(" . $t['line'] . ")");
}
print("<div style='background-color: #ffaaaa;'>");
print("<h3>" . $e->getMessage() . "</h3>");
print("At Line " . $e->getLine() . " of " . $e->getFile() . "<br>");
$line = $e->getLine() - 1;
$pre = ErrorHandler::getFileLines($e->getFile(), $line - 5, $line - 1);
$line = ErrorHandler::getFileLines($e->getFile(), $line, $line);
$post = ErrorHandler::getFileLines($e->getFile(), $line + 1, $line + 5);
print("<pre>");
foreach ($pre as $l) {
print($l . "\n");
}
print($line[0] . " <---\n");
foreach ($post as $l) {
print($l . "\n");
}
print("</pre>");
print("</div>");
$trace = $e->getTrace();
array_shift($trace);
foreach ($trace as $t) {
if (array_key_exists("class", $t) && array_key_exists("function", $t)) {
print("<div>");
print("<h5>From " . $t['class'] . "::" . $t['function'] . "</h5>");
print("At Line " . $t['line'] . " of " . $t['file'] . "<br>");
print("</div>");
}
}
}
public static function handleError($num, $str, $file, $line, $context = null) {
ErrorHandler::handleException(new ErrorException($str, 0, $num, $file, $line));
}
public static function hook() {
if (Config::get("DEBUG")) {
ini_set("display_errors", "on");
error_reporting(E_ALL);
} else {
ini_set("display_errors", "off");
}
// register_shutdown_function("ErrorHandler::checkForFatalCrash");
// set_error_handler("ErrorHandler::handleError");
// set_exception_handler("ErrorHandler::handleException");
}
}
Executable
+128
View File
@@ -0,0 +1,128 @@
<?php
class File {
private $_path = null;
private $_headers = [];
public function __construct($path = null) {
if ($path instanceof File) {
$this->_path = $path->path();
}
$this->_path = $path;
}
public function basename() {
return pathinfo($this->_path, PATHINFO_BASENAME);
}
public function dirname() {
return pathinfo($this->_path, PATHINFO_DIRNAME);
}
public function extension() {
return pathinfo($this->_path, PATHINFO_EXTENSION);
}
public function filename() {
return pathinfo($this->_path, PATHINFO_FILENAME);
}
public function delete() { $this->unlink(); }
public function unlink() {
unlink($this->_path);
}
public function content() {
return get_file_contents($this->_path);
}
public function emit() {
$headers = $this->_headers;
$headers['ETag'] = "\"" . $this->hash() . "\"";
foreach ($headers as $k=>$v) {
header($k . ": " . $v);
}
readfile($this->_path);
}
public function set_headers($data) {
$this->_headers = $data;
}
public function get_headers() {
return $this->_headers;
}
public function set_header($k, $v) {
$this->_headers[trim($k)] = trim($v);
}
public function exists() {
return file_exists($this->_path);
}
public function size() {
return stat($this->_path)["size"];
}
public function path() {
return $this->_path;
}
public function mime() {
if (!file_exists($this->_path)) {
return false;
}
return mime_content_type($this->_path);
}
public function __toString() {
return $this->_path;
}
public function parent() {
$p = $this->dirname();
if ($p == "") {
return false;
}
return new File($p);
}
public function mkdir() {
$p = $this->parent();
if ($p) {
if (!$p->exists()) {
$p->mkdir();
}
}
if (!$this->exists()) {
mkdir($this->_path, 0777);
}
}
public function hash($type = "sha256") {
return hash_file($type, $this->_path);
}
public function rename($to, $over=false) {
if ($to == $this->path()) return false;
if ((!$over) && file_exists($to)) return false;
copy($this->path(), $to);
if (!file_exists($to)) return false;
unlink($this->path());
$this->_path = $to;
return true;
}
public function get_chunk($from, $len) {
$f = fopen($this->_path, "r");
fseek($f, $from);
$data = fread($f, $len);
fclose($f);
return $data;
}
}
Executable
+44
View File
@@ -0,0 +1,44 @@
<?php
class Form {
public static function input($title, $name, $value, $attrs = []) {
$at = [];
foreach ($attrs as $k=>$v) {
$at[] = "$k = \"$v\"";
}
return blade("forms.input", [
"title" => $title,
"name" => $name,
"value" => $value,
"attrs" => implode(" ", $at)
]);
}
public static function password($title, $name, $value, $attrs = []) {
$at = [];
foreach ($attrs as $k=>$v) {
$at[] = "$k = \"$v\"";
}
return blade("forms.password", [
"title" => $title,
"name" => $name,
"value" => $value,
"attrs" => implode(" ", $at)
]);
}
public static function submit($title, $attrs = []) {
$at = [];
foreach ($attrs as $k=>$v) {
$at[] = "$k = \"$v\"";
}
return blade("forms.submit", [
"title" => $title,
"attrs" => implode(" ", $at)
]);
}
}
+289
View File
@@ -0,0 +1,289 @@
<?php
require_once(__DIR__ . "/File.php");
require_once(__DIR__ . "/HTTPRequest.php");
class Gemini {
public $file_uri = "";
public $root = "https://generativelanguage.googleapis.com";
public $verbose = false;
private $_upload_callback = false;
private $_process_callback = false;
public $parameters = [];
public $attachments = [];
public function add_parameter($param) {
$this->parameters[$param->name] = $param;
}
public function get_parameters() {
$o = [];
foreach ($this->parameters as $name=>$param) {
$o[$name] = $param->get_object();
}
return $o;
}
public function get_required() {
$o = [];
foreach ($this->parameters as $name=>$param) {
if ($param->required) {
$o[] = $name;
}
}
return $o;
}
public static function get_key() {
Config::refresh();
$keys = Config::get("GEMINI_KEY");
if (!is_array($keys)) return $keys;
return $keys[rand(0, count($keys)-1)];
}
public function attach($name, File $file) {
$o = new stdClass;
$o->uri = null;
$o->name = $name;
$o->file = $file;
$this->attachments[$name] = $o;
}
public function upload_file($name, File $file, $key = false) {
if ($file->size() > 50000000) {
throw new Exception("File too large");
}
set_time_limit(300);
if ($this->verbose) {
print("Uploading file...\n");
flush();
}
$cb = false;
if ($this->_upload_callback) {
$cb = $this->_upload_callback;
}
if ($cb) $cb(0);
if ($key == false) {
$key = Gemini::get_key();
}
$json = "{'file': {'display_name': '" . $name . "'}}";
if ($this->verbose) {
print("Upload data: " . $json . "\n");
flush();
}
$h = new HTTPRequest();
$r = $h->post($this->root . "/upload/v1beta/files", $json, [
"x-goog-api-key: " . $key,
"Content-Type: application/json",
"X-Goog-Upload-Protocol: resumable",
"X-Goog-Upload-Command: start",
"X-Goog-Upload-Header-Content-Length: " . $file->size(),
"X-Goog-Upload-Header-Content-Type: " . $file->mime()
]);
if ($this->verbose) {
print("Request data:\n");
print_r($h);
flush();
}
if ($r != 200) {
throw new Exception("Error uploading file to Gemini", $r);
}
$url = $h->headers['x-goog-upload-url'];
$chunksize = $h->headers['x-goog-upload-chunk-granularity'];
$size = $file->size();
$s = $size;
$pos = 0;
while ($s > 0) {
$chunk = min($s, $chunksize);
$pct = round($pos / $size * 100);
if ($cb) $cb($pct);
if ($this->verbose) {
printf("***** %d%%\n", round($pct));
flush();
}
$data = $file->get_chunk($pos, $chunk);
$s -= $chunk;
$fin = $s == 0 ? ", finalize" : "";
$r = $h->post($url, $data, [
"Content-Length: " . $chunk,
"X-Goog-Upload-Offset: " . $pos,
"X-Goog-Upload-Command: upload$fin"
]);
if ($r != 200) {
throw new Exception("Error uploading chunk $pos to Gemini", $r);
}
if ($this->verbose) {
print_r($r);
flush();
}
$pos += $chunk;
}
$d = json_decode($h->body);
if ($this->verbose) {
print("Final returned body:\n");
print_r($d);
}
if ($cb) $cb(100);
return $d->file->uri;
}
public function geminiOverview() {
$cb = false;
if ($this->_process_callback) $cb = $this->_process_callback;
if ($cb) $cb("Uploading files");
if ($this->verbose) {
print("<pre>");
}
$key = Gemini::get_key();
if (count($this->attachments) == 0) {
throw new Exception("No files to upload");
}
foreach ($this->attachments as $name=>$file) {
$file->uri = $this->upload_file($name, $file->file, $key);
}
$system = [];
$system[] = "Summarize the attached PDF document.";
$parts = [];
$text = new stdClass;
$text->text = implode("\n", $system);
$parts[] = $text;
foreach ($this->attachments as $name=>$file) {
$f = new stdClass;
$f->file_data = new stdClass;
$f->file_data->mime_type = $file->file->mime();
$f->file_data->file_uri = $file->uri;
$parts[] = $f;
}
$ob = [
"contents" => [[
"parts" => $parts
]],
"generationConfig" => [
"responseMimeType" => "application/json",
"responseSchema" => [
"type" => "object",
"properties" => $this->get_parameters(),
"required" => $this->get_required(),
]
]
];
$json = json_encode($ob, JSON_PRETTY_PRINT);
if ($this->verbose) {
print("AI reuqest: " . $json . "\n");
flush();
}
set_time_limit(300);
if ($cb) $cb("Sending request");
$h = new HTTPRequest();
$r = $h->post($this->root . "/v1beta/models/" . Config::get("GEMINI_MODEL") . ":generateContent", $json, [
"x-goog-api-key: " . $key,
"Content-Type: application/json"
]);
if ($cb) $cb("Processing response");
if ($this->verbose) {
print("AI Response:\n");
print_r($h);
flush();
}
$resp = json_decode($h->body);
if ($r != 200) {
if (@$resp->error) {
throw new Exception($resp->error->message, $resp->error->code);
} else {
throw new Exception("HTTP Error $r requesting AI assistance", $r);
}
}
if (property_exists($resp, "candidates")) {
$text = $resp->candidates[0]->content->parts[0]->text;
$lines = explode("\n", $text);
$lastLine = "";
$out = [];
foreach ($lines as $line) {
if (str_starts_with($line, "* ") and !str_starts_with($lastLine, "* ")) {
$lastLine = $line;
$line = "\n" . $line;
} else if (str_starts_with($line, "1. ") and (trim($lastLine) != "")) {
$lastLine = $line;
$line = "\n" . $line;
} else {
$lastLine = $line;
}
$out[] = $line;
}
return implode("\n", $out);
}
throw new Exception("Content missing processing AI data", 0);
}
public function upload_callback(callable $cb) {
$this->_upload_callback = $cb;
}
public function process_callback(callable $cb) {
$this->_process_callback = $cb;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
class GeminiParameter {
public $name;
public $type;
public $description;
public $required;
public $children;
public $enum;
public function __construct($n, $t, $d, $r = false) {
$this->name = $n;
$this->type = $t;
$this->description = $d;
$this->required = $r;
$this->children = [];
$this->enum = [];
}
public function add_enum($e) {
$this->enum[] = $e;
}
public function add_child($n, $t, $d, $r = false) {
$p = new GeminiParameter($n, $t, $d, $r);
$this->children[$n] = $p;
return $p;
}
public function get_object() {
$o = new stdClass;
$o->type = $this->type;
$o->description = $this->description;
switch ($this->type) {
case "string":
if (count($this->enum) > 0) {
$o->format = "enum";
$o->enum = $this->enum;
}
break;
case "object":
$o->properties = [];
$o->required = [];
foreach ($this->children as $cn=>$c) {
$o->properties[$cn] = $c->get_object();
if ($c->required) {
$o->required[] = $cn;
}
}
break;
}
return $o;
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
class HTTPRequest {
public $body = "";
public $headers = [];
public $status = 0;
public $version = "1.0";
public $connect_timeout = 2;
public $transfer_timeout = 300;
public $ch = null;
public function __construct() {
$this->ch = curl_init();
}
public function get($url, $headers = [], $headersonly = false) {
curl_reset($this->ch);
curl_setopt($this->ch, CURLOPT_URL, $url);
curl_setopt($this->ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0");
curl_setopt($this->ch, CURLOPT_FILETIME, true);
curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout);
curl_setopt($this->ch, CURLOPT_TIMEOUT, $this->transfer_timeout);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_NOBODY, $headersonly);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_HEADER, true);
curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
$this->status = 0;
$response = curl_exec($this->ch);
$header_size = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$this->body = substr($response, $header_size);
$this->headers = [];
foreach ($hl as $h) {
$h = trim($h);
if (preg_match('/^HTTP\/([^\s]+)\s+(\d+)\s*/', $h, $m)) {
$this->version = $m[1];
$this->status = $m[2];
continue;
}
if (str_starts_with($h, " ")) {
$this->headers[$curr] .= " " . trim($h);
continue;
}
if (preg_match('/^([^:]+):\s+(.*)$/', $h, $m)) {
$curr = strtolower($m[1]);
$this->headers[$curr] = $m[2];
}
}
return $this->status;
}
public function post($url, $data, $headers = [], $headersonly = false) {
curl_reset($this->ch);
curl_setopt($this->ch, CURLOPT_URL, $url);
curl_setopt($this->ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0");
curl_setopt($this->ch, CURLOPT_POST, true);
curl_setopt($this->ch, CURLOPT_FILETIME, true);
curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, $this->connect_timeout);
curl_setopt($this->ch, CURLOPT_TIMEOUT, $this->transfer_timeout);
curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($this->ch, CURLOPT_NOBODY, $headersonly);
curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->ch, CURLOPT_HEADER, true);
curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($this->ch, CURLOPT_POSTFIELDS, $data);
$this->status = 0;
$response = curl_exec($this->ch);
$header_size = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$this->body = substr($response, $header_size);
$this->headers = [];
$hl = explode("\n", $header);
foreach ($hl as $h) {
$h = trim($h);
if (preg_match('/^HTTP\/([^\s]+)\s+(\d+)\s*/', $h, $m)) {
$this->version = $m[1];
$this->status = $m[2];
continue;
}
if (str_starts_with($h, " ")) {
$this->headers[$curr] .= " " . trim($h);
continue;
}
if (preg_match('/^([^:]+):\s+(.*)$/', $h, $m)) {
$curr = strtolower($m[1]);
$this->headers[$curr] = $m[2];
}
}
return $this->status;
}
}
Executable
+124
View File
@@ -0,0 +1,124 @@
<?php
require_once(__DIR__ . "/File.php");
class Image extends File {
private $_img = null;
private $_type = null;
public function __construct($w, $h = null) {
if ($w instanceof File) {
$w = $w->path();
}
if ($h === null) {
parent::__construct($w);
$this->_type = $this->mime();
} else {
$this->_img = ImageCreateTrueColor($w, $h);
}
}
public function getImage() {
if ($this->_img == null) {
if ($this->_type === false) {
$this->_img = ImageCreateTrueColor(1, 1);
} else {
switch ($this->_type) {
case "image/png":
$this->_img = ImageCreateFromPNG($this->path());
break;
case "image/jpeg":
$this->_img = ImageCreateFromJPEG($this->path());
break;
case "image/gif":
$this->_img = ImageCreateFromGIF($this->path());
break;
}
}
}
return $this->_img;
}
public function save($fn = null, $type = null) {
$newimg = true;
if ($fn == null) {
$fn = $this->path();
$newimg = false;
}
if ($type == null) {
$type = $this->_type;
$newimg = false;
}
switch ($type) {
case "image/png":
ImagePNG($this->getImage(), $fn);
break;
case "image/jpeg":
ImageJPEG($this->getImage(), $fn);
break;
case "image/gif":
ImageGIF($this->getImage(), $fn);
break;
default:
throw new Exception("Invalid mime type $type specified");
}
if ($newimg) {
return new Image($fn);
}
return $this;
}
public function width() {
return ImageSX($this->getImage());
}
public function height() {
return ImageSY($this->getImage());
}
public function scale($w, $h = 0) {
$sx = $this->width();
$sy = $this->height();
$dx = $w;
if ($h == 0) {
$aspect = $sx / $sy;
$dy = (int)($dx / $aspect);
} else {
$dh = $h;
}
$new = ImageCreateTrueColor($dx, $dy);
ImageCopyResampled($new, $this->getImage(), 0, 0, 0, 0, (int)$dx, (int)$dy, (int)$sx, (int)$sy);
ImageDestroy($this->getImage());
$this->_img = $new;
}
public function emit() {
$this->set_header("Content-Type", $this->_type);
$this->set_header("Cache-Control", "public, max-age=86400, must-revalidate");
$this->set_header("Content-Disposition", "inline; filename=" . $this->basename());
parent::emit();
}
public function color($r, $g, $b) {
return ImageColorAllocate($this->getImage(), $r, $g, $b);
}
public function clear($c) {
ImageFilledRectangle($this->getImage(), 0, 0, $this->width(), $this->height(), $c);
}
public function text($x, $y, $t, $c, $f) {
ImageString($this->getImage(), $f, $x, $y, $t, $c);
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
abstract class Job {
private $_jobid = 0;
private $_source = "unknown";
private $_owner = "system";
public $retries = 0;
public function __construct($source = "unknown") {
$this->_source = $source;
}
// Implement this function to actually do the job
abstract public function run();
// Place the current job onto the queue ready for processing.
public function queue($owner = -1) {
$class = get_called_class();
$data = serialize($this);
$db = DB::getInstance();
if ($owner == -1) {
$user = get_user();
if ($user) {
$owner = $user->id;
}
}
$this->_jobid = $db->insert("job", [
"class" => $class,
"data" => $data,
"queued" => time(),
"source" => $this->_source,
"status" => "Queued",
"owner" => $owner
]);
return $this->_jobid;
}
public function getJobClass() {
$class = get_called_class();
return $class;
}
public function jobID() {
return $this->_jobid;
}
public function setJobID($id) {
$this->_jobid = $id;
}
// Set the current status message
public function status($txt) {
$db = DB::getInstance();
$db->update("job", $this->_jobid, [
"status" => substr($txt, 0, 200)
]);
if (is_terminal()) {
print(" => " . $txt . "\n");
}
}
// Mark the job as completed successfully
public function finish() {
$db = DB::getInstance();
$db->update("job", $this->_jobid, [
"finished" => time(),
]);
}
// Mark the job as failed miserably
public function fail($message = "") {
$db = DB::getInstance();
if ($message != "") {
$db->update("job", $this->_jobid, [
"status" => substr($message, 0, 200),
"failed" => time(),
]);
} else {
$db->update("job", $this->_jobid, [
"failed" => time(),
]);
}
}
// Restart the job.
public function retry($msg = "", $when = -1) {
if ($when == -1) {
$when = rand(60, 600);
}
$db = DB::getInstance();
$status = "Retrying " . date("Y-m-d H:i", time() + $when);
if ($msg != "") {
$status .= " ($msg)";
}
$db->update("job", $this->_jobid, [
"failed" => 0,
"finished" => 0,
"started" => 0,
"status" => $status,
"retry" => time() + $when
]);
}
// Restart the job and defer it to a later time.
public function defer($when) {
$db = DB::getInstance();
$db->update("job", $this->_jobid, [
"failed" => 0,
"finished" => 0,
"started" => 0,
"queued" => $when,
"status" => "Deferred until " . gmdate("Y-m-d\TH:i:s\Z", $when)
]);
}
// Look for the next available job that optionally has
// the requested class. Mark it as started, deserialize
// it, and return the job runner object.
// Returns false if no job available.
public static function consumeNextJob($class = null) {
$db = DB::getInstance();
$db->query("lock table job write");
if ($class == null) {
$q = $db->query("select * from job where started=0 and (retry=0 or retry < unix_timestamp(now())) and queued < unix_timestamp(now()) order by queued limit 1");
} else {
$q = $db->query("select * from job where started=0 and (retry=0 or retry < unix_timestamp(now())) and queued < unix_timestamp(now()) and class=:class order by queued limit 1", ["class" => $class]);
}
$r = $db->nextRecord($q);
if (!$r) {
$db->query("unlock tables");
return false;
}
$db->update("job", $r->id, [
"started" => time()
]);
$db->query("unlock tables");
$ob = unserialize($r->data);
$ob->setJobID($r->id);
$ob->setOwner($r->owner);
return $ob;
}
public function getOwner() {
return $this->_owner;
}
public function setOwner($owner) {
$this->_owner = $owner;
}
}
Executable
+501
View File
@@ -0,0 +1,501 @@
<?php
// Special data types
define("MODEL_SERIAL", 0x1000); // auto increment id
define("MODEL_OBJECT", 0x1001); // Link to another object type by id
define("MODEL_CREATED", 0x1002); // CREATED timestamp
define("MODEL_UPDATED", 0x1003); // UPDATED timestamp
// Numeric data types
define("MODEL_BIT", 0x2000);
define("MODEL_TINYINT", 0x2001);
define("MODEL_BOOL", 0x2002);
define("MODEL_SMALLINT", 0x2003);
define("MODEL_MEDIUMINT", 0x2004);
define("MODEL_INT", 0x2005);
define("MODEL_BIGINT", 0x2006);
define("MODEL_FLOAT", 0x2007);
define("MODEL_DOUBLE", 0x2008);
define("MODEL_DECIMAL", 0x2009);
// String data types
define("MODEL_CHAR", 0x3000);
define("MODEL_VARCHAR", 0x3001);
define("MODEL_BINARY", 0x3002);
define("MODEL_VARBINARY", 0x3003);
define("MODEL_TINYBLOB", 0x3004);
define("MODEL_TINYTEXT", 0x3005);
define("MODEL_TEXT", 0x3006);
define("MODEL_BLOB", 0x3007);
define("MODEL_MEDIUMTEXT", 0x3008);
define("MODEL_MEDIUMBLOB", 0x3009);
define("MODEL_LONGTEXT", 0x300A);
define("MODEL_LONGBLOB", 0x300B);
define("MODEL_ENUM", 0x300C);
define("MODEL_SET", 0x300D);
// Date types
define("MODEL_DATE", 0x4000);
define("MODEL_DATETIME", 0x4001);
define("MODEL_TIMESTAMP", 0x4002);
define("MODEL_TIME", 0x4003);
define("MODEL_YEAR", 0x4004);
define("MODEL_INDEX", 12);
define("MODEL_FULLTEXT", 13);
define("MODEL_UNIQUE", 14);
class Model implements JsonSerializable {
public $_fields = []; // List of fields in the DB table
public $_virgin = []; // Virgin, untouched data loaded from the DB
public $_data = []; // Current (modified) data as stored in the DB
public $_objects = []; // Objects constructed from the values in _data
public $_timeout = 1;
public $_valid = false;
public $_loaded = false;
public static $_cache = null;
static protected $_raw = false;
public function __construct($id = null, $deep = true) {
$this->_valid = false;
if ($id !== null) {
$this->_load_record($id);
$this->_loaded = $this->_valid;
} else {
$this->_get_fields();
$this->_loaded = false;
$this->_valid = true;
}
}
public static function cache_connect($host, $port, $weight = 50) {
if (Model::$_cache === null) {
Model::$_cache = new Memcached();
}
Model::$_cache->addServer($host, $port, $weight);
}
private function _load_record($id) {
$class = get_called_class();
if (property_exists($class, "table")) {
$v = get_class_vars($class);
$table = $v["table"];
} else {
$table = strtolower($class);
}
$r = DB::getInstance()->select($table, $id);
if ($r === false) {
$this->_valid = false;
return;
}
foreach ($r as $k=>$v) {
$this->_fields[] = $k;
$this->_virgin[$k] = $v;
}
if (property_exists($class, "transform")) {
foreach ($this->_fields as $k) {
if (array_key_exists($k, $this->transform)) {
$v = $this->transform[$k];
$f = "from_" . $v;
if (method_exists($this, $f)) {
$this->_data[$k] = $this->$f($this->_virgin[$k]);
} else {
$this->_data[$k] = $this->_virgin[$k];
}
} else {
$this->_data[$k] = $this->_virgin[$k];
}
}
} else {
foreach ($this->_fields as $k) {
$this->_data[$k] = $this->_virgin[$k];
}
}
$this->_valid = true;
}
public static function find($where = []) {
$class = get_called_class();
if (property_exists($class, "table")) {
$v = get_class_vars($class);
$table = $v["table"];
} else {
$table = strtolower($class);
}
return new Query($class, $table, $where);
}
private function _get_fields() {
$class = get_called_class();
if ($class == "Model") { // Light model being used for xfer
return;
}
if (property_exists($class, "table")) {
$v = get_class_vars($class);
$table = $v["table"];
} else {
$table = strtolower($class);
}
$this->_fields = [];
$q = DB::getInstance()->query("describe `" . $table . "`");
while ($r = DB::getInstance()->nextRecord($q)) {
$this->_fields[] = $r->Field;
$this->_data[$r->Field] = null;
}
$this->_valid = true;
}
public function from_json($data) {
if ($data) {
$d = json_decode($data);
if ($d) return $d;
}
return new stdClass;
}
public function to_json($data) {
return json_encode($data);
}
public function valid() {
return $this->_valid;
}
public function loaded() {
return $this->_loaded;
}
public function save() {
Model::raw_on();
$class = get_called_class();
if (property_exists($class, "table")) {
$v = get_class_vars($class);
$table = $v["table"];
} else {
$table = strtolower($class);
}
$_save = [];
if (property_exists($class, "transform")) {
foreach ($this->_fields as $k) {
if (array_key_exists($k, $this->transform)) {
$v = $this->transform[$k];
$f = "to_" . $v;
if (method_exists($this, $f)) {
$_save[$k] = $this->$f(@$this->$k);
} else {
$v = __unref($this->$k);
if (is_string($v)) $v = trim($v);
$_save[$k] = $v;
}
} else {
$v = __unref($this->$k);
if (is_string($v)) $v = trim($v);
$_save[$k] = $v;
}
}
} else {
foreach ($this->_fields as $k) {
$v = __unref($this->$k);
if (is_string($v)) $v = trim($v);
$_save[$k] = $v;
}
}
$_update = [];
foreach ($_save as $k=>$v) {
if (array_key_exists($k, $this->_virgin)) {
if ($_save[$k] != $this->_virgin[$k]) {
$_update[$k] = $v;
$this->_virgin[$k] = $v;
}
} else {
$_update[$k] = $v;
$this->_virgin[$k] = $v;
}
}
$triggers = [];
if ($this->_loaded) {
if (count($_update) > 0) {
if (in_array("updated", $this->_fields)) {
$_update['updated'] = time();
}
DB::getInstance()->update($table, $this->id, $_update);
}
} else {
$_update = $this->_virgin;
if (in_array("created", $this->_fields)) {
$_update['created'] = time();
}
$this->id = DB::getInstance()->insert($table, $_update);
$this->_loaded = true;
}
foreach ($_update as $k=>$v) {
if (property_exists($class, "_triggers")) {
if (array_key_exists($k, $this->_triggers)) {
$triggers[$this->_triggers[$k]] = 1;
}
}
}
foreach ($triggers as $trigger=>$count) {
if ($trigger instanceof \Closure) {
$trigger->call($this);
continue;
}
if (is_array($trigger)) {
$c = $trigger[0];
$f = $trigger[1];
$c::$f();
continue;
}
if (is_string($trigger)) {
$this->$trigger();
continue;
}
}
Model::raw_off();
}
public static function raw_on() {
Model::$_raw = true;
}
public static function raw_off() {
Model::$_raw = false;
}
static public function raw() {
return Model::$_raw;
}
public function delete() {
$class = get_called_class();
if (property_exists($class, "table")) {
$v = get_class_vars($class);
$table = $v["table"];
} else {
$table = strtolower($class);
}
if (method_exists($this, "on_delete")) {
$this->on_delete();
}
DB::getInstance()->query("delete from `" . $table . "` where id=:id", ["id" => $this->_data["id"]]);
}
public function __toString() {
return "" . $this->_data["id"];
}
public function __toInt() {
return (int)$this->_data["id"];
}
public function jsonSerialize() : mixed {
$out = [];
foreach ($this->_fields as $f) {
$out[$f] = $this->_data["$f"];
}
foreach ($this->_fields as $f) {
if (array_key_exists($f, $this->_objects)) {
$out[$f] = $this->_objects["$f"];
}
}
if (property_exists($this, "_computed")) {
foreach ($this->_computed as $k=>$v) {
$out[$k] = $this->$v();
}
}
return $out;
}
private function __get_field_class($f) {
if (!property_exists($this, "_classes")) return null;
if (!array_key_exists($f, $this->_classes)) return null;
return $this->_classes[$f];
}
public function __get($k) {
if (property_exists($this, "_computed")) {
if (array_key_exists($k, $this->_computed)) {
$c = $this->cache_get($k);
if ($c) return $c;
$func = $this->_computed[$k];
$c = $this->$func();
$this->cache_set($k, $c);
return $c;
}
}
if (!in_array($k, $this->_fields)) {
return null;
}
$class = $this->__get_field_class($k);
if ($class != null) {
if (array_key_exists($k, $this->_objects)) return $this->_objects[$k];
}
return $this->_data[$k];
}
public function __set($key, $val) {
if (!in_array($key, $this->_fields)) return false;
$class = $this->__get_field_class($key);
if ($class != null) {
if ($val instanceof Model) {
if ($val instanceof $class) {
$this->_data[$val] = $val->id;
$this->_objects[$key] = $val;
return true;
}
throw new Exception('Class mismatch');
return false;
}
if (is_numeric($val)) {
$this->_data[$key] = $val;
$this->_objects[$key] = new $class($val);
return true;
}
$val = (int)$val;
$this->_data[$key] = $val;
$this->_objects[$key] = new $class($val);
return true;
}
$this->_data[$key] = $val;
unset($this->_objects[$key]);
return true;
}
public function __isset($key) {
if (!in_array($key, $this->_fields)) return false;
return true;
}
public function __unset($key) {
if (!in_array($key, $this->_fields)) return;
$this->_data[$key] = null;
unset($this->_objects[$key]);
}
public function load($key = null) {
if ($key === null) {
foreach ($this->_fields as $key) {
$this->load($key);
}
return true;
}
if (!in_array($key, $this->_fields)) {
return false;
}
$class = $this->__get_field_class($key);
if ($class == null) {
return false;
}
if ($this->_data[$key] === null) {
return false;
}
$ob = new $class($this->_data[$key]);
if (!$ob->valid()) return false;
$this->_objects[$key] = $ob;
return true;
}
private function cache_key($key) {
$class = get_called_class();
return sprintf("%s[%d]::%s", $class, $this->id, $key);
}
public function cache_set($key, $val) {
$key = $this->cache_key($key);
Model::$_cache->set($key, $val, $this->_timeout);
}
public function cache_get($key) {
$key = $this->cache_key($key);
return Model::$_cache->get($key);
}
public function cache_invalidate($key) {
$key = $this->cache_key($key);
Model::$_cache->delete($key);
}
public static function get_all_models() {
$res = [];
foreach (get_declared_classes() as $class) {
if (is_subclass_of($class, "Model")) {
$res[] = $class;
}
}
}
public function create_or_update_table() {
}
public function get_table_name() {
$class = get_called_class();
if (property_exists($class, "table")) {
$v = get_class_vars($class);
return($v["table"]);
}
return (strtolower($class));
}
public function last_modified() {
if ($this->updated > 0) {
return date("Y-m-d H:i:s", $this->updated);
} else {
return date("Y-m-d H:i:s", $this->created);
}
}
}
Executable
+165
View File
@@ -0,0 +1,165 @@
<?php
require_once(__DIR__ . "/File.php");
define("PDF_SCREEN", 0);
define("PDF_EBOOK", 1);
define("PDF_PRINT", 2);
define("PDF_PREPRESS", 3);
class PDF extends File {
private $_force_download = false;
private $_fake_filename = null;
public function __construct($f) {
parent::__construct($f);
}
public function extract_page($page, $file, $dpi = 300) {
$m = Config::get("MAGICK");
$p = new Process($m);
$p->arg("-density"); $p->arg($dpi);
$p->arg(sprintf("%s[%d]", $this->path(), $page));
$p->arg("-alpha"); $p->arg("remove");
$p->arg($file);
$rv = $p->execute();
if ($rv != 0) {
$img = new Image(640, 640);
$y = 40;
$x = 10;
$b = $img->color(255,255,255);
$f = $img->color(0, 0, 0);
$img->clear($b);
$e = implode("\n", $p->stderr());
$e = wordwrap($e, 80);
$e = explode("\n", $e);
foreach ($e as $l) {
$img->text($x, $y, $l, $f, 3);
$y += 20;
}
$img->save($file, "image/jpeg");
return $img;
}
return new Image($file);
}
public function force_download() {
$this->_force_download = true;
}
public function fake_filename($f) {
$this->_fake_filename = $f;
}
public function emit() {
$filename = $this->_fake_filename;
if ($filename == null) {
$filename = $this->basename();
}
if ($this->_force_download) {
$this->set_header("Content-Type", "application/octet-stream");
$this->set_header("Cache-Control", "public, max-age=31560000, immutable");
$this->set_header("Content-Disposition", "attachment; filename=\"$filename\"");
} else {
$this->set_header("Content-Type", "application/pdf");
$this->set_header("Cache-Control", "public, max-age=31560000, immutable");
$this->set_header("Content-Disposition", "inline; filename=\"$filename\"");
}
parent::emit();
}
public function info() {
$infolines = array();
$info = array();
$proc = new Process("pdfinfo");
$proc->arg($this->path());
$proc->execute();
$infolines = $proc->stdout();
foreach ($infolines as $line) {
if (preg_match('/^([^:]+):\s+(.*)$/', $line, $m)) {
$info[$m[1]] = $m[2];
}
}
return $info;
}
public function geminiOverview($prods = null, $verbose=false) {
$g = new Gemini();
$g->verbose = $verbose;
try {
return $g->geminiOverview($this, $prods);
} catch (Exception $e) {
throw $e;
}
}
public function recompress($dest, $size=PDF_SCREEN) {
$proc = new Process("gs");
$proc->arg("-sDEVICE=pdfwrite");
$proc->arg("-dCompatibilityLevel=1.6");
switch ($size) {
case PDF_SCREEN:
$proc->arg("-dPDFSETTINGS=/screen");
break;
case PDF_EBOOK:
$proc->arg("-dPDFSETTINGS=/ebook");
break;
case PDF_PRINT:
$proc->arg("-dPDFSETTINGS=/printer");
break;
case PDF_PREPRESS:
$proc->arg("-dPDFSETTINGS=/prepress");
break;
}
$proc->arg("-dNOPAUSE");
$proc->arg("-dQUIET");
$proc->arg("-dBATCH");
$proc->arg("-sOutputFile=" . $dest);
$proc->arg($this->path());
$rv = $proc->execute();
if ($rv == 0) {
return new PDF($dest);
}
return false;
}
public function ocr(File $out) {
$proc = new Process("ocrmypdf");
$proc->arg($this->path());
$proc->arg($out->path());
$proc->execute();
}
public function text() {
$proc = new Process("pdftotext");
# $proc->arg("-layout");
$proc->arg("-enc");
$proc->arg("ASCII7");
$proc->arg($this->path());
$proc->arg("-");
$proc->execute();
$text = new Text(implode("\n", $proc->stdout()));
return $text;
}
}
+82
View File
@@ -0,0 +1,82 @@
<?php
class Process {
private $_command = null;
private $_args = [];
private $_env = [];
private $_cwd = null;
private $_pipes = [];
private $_stdout = [];
private $_stderr = [];
private $_fd = null;
private $_status = null;
public function __construct($command) {
$this->_command = $command;
$this->_env = $_ENV;
}
public function arg($v) {
$this->_args[] = $v;
}
public function env($k, $v) {
$this->_env[$k] = $v;
}
public function cwd($p) {
$this->_cwd = $p;
}
public function execute() {
$command = [];
$command[] = escapeshellcmd($this->_command);
foreach ($this->_args as $a) {
$command[] = escapeshellarg($a);
}
$desc = [
["pipe", "r"],
["pipe", "w"],
["pipe", "w"]
];
$this->_fd = proc_open(
implode(" ", $command),
$desc,
$this->_pipes,
$this->_cwd,
$this->_env
);
fclose($this->_pipes[0]); // stdin
$this->_status = proc_get_status($this->_fd);
while ($this->_status["running"]) {
$this->_stdout[] = stream_get_contents($this->_pipes[1]);
$this->_stderr[] = stream_get_contents($this->_pipes[2]);
$this->_status = proc_get_status($this->_fd);
}
proc_close($this->_fd);
return $this->_status["exitcode"];
}
public function stdout() {
return explode("\n", implode("", $this->_stdout));
}
public function stderr() {
return explode("\n", implode("", $this->_stderr));
}
}
Executable
+112
View File
@@ -0,0 +1,112 @@
<?php
class Query {
private $class = "";
private $table = "";
private $where = [];
private $order = [];
private $limit = "";
private $query;
public function __construct($class, $table, $where = []) {
$this->where = $where;
$this->table = $table;
$this->class = $class;
}
public function where($where) {
foreach ($where as $w) {
$this->where[] = $w;
}
return $this;
}
public function orderBy($order) {
$this->order[] = $order;
return $this;
}
public function orderByDesc($order) {
$this->order[] = "$order desc";
return $this;
}
public function limit($a, $b=null) {
if ($b == null) {
$this->limit = $a;
} else {
$this->limit = "$a,$b";
}
return $this;
}
function all() {
$this->__run();
$cl = $this->class;
$out = new Collection;
while ($r = DB::getInstance()->nextRecord($this->query)) {
$ob = new $cl($r->id);
$out->push($ob);
}
return $out;
}
function first() {
$this->__run();
$cl = $this->class;
$out = new Collection;
if ($r = DB::getInstance()->nextRecord($this->query)) {
$ob = new $cl($r->id);
return $ob;
}
return false;
}
function next() {
$cl = $this->class;
$out = new Collection;
if ($r = DB::getInstance()->nextRecord($this->query)) {
$ob = new $cl($r->id);
return $ob;
}
return false;
}
// Run the query but don't retrieve anything.
private function __run() {
$args = [];
$ac = 1;
$q = "select id from `" . $this->table . "`";
if (count($this->where) > 0) {
$q .= " where";
$first = true;
foreach ($this->where as $w) {
if (!$first) {
$q .= " and";
}
$q .= sprintf(" `%s` %s :arg%d", $w[0], $w[1], $ac);
$args["arg" . $ac] = $w[2];
$first = false;
$ac++;
}
}
if (count($this->order) > 0) {
$q .= " order by ";
$q .= implode(",", $this->order);
}
if ($this->limit != "") {
$q .= sprintf(" limit %s", $this->limit);
}
$this->query = DB::getInstance()->query($q, $args);
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
class Request {
private $type = "";
private $headers = [];
private $referer = "";
private $get = [];
private $post = [];
private $put = [];
private $peer = "";
private $path = "";
private $route = null;
private $files = [];
public function __construct() {
$this->get = $_GET;
$this->post = $_POST;
$this->files = $_FILES;
$this->type = strtoupper($_SERVER['REQUEST_METHOD']);
$this->peer = $_SERVER['REMOTE_ADDR'];
if (array_key_exists("REDIRECT_URL", $_SERVER)) {
$this->path = $_SERVER['REDIRECT_URL'];
} else {
$this->path = "/";
}
if ($this->type == "PUT") {
$data = file_get_contents("php://input");
$arr = [];
parse_str($data, $arr);
foreach ($arr as $k=>$v) {
$this->put[$k] = $v;
}
}
foreach ($_SERVER as $k=>$v) {
if (str_starts_with($k, "HTTP_")) {
$header = substr($k, 5);
$header = strtolower($header);
$header = str_replace("_", " ", $header);
$header = ucwords($header);
$header = str_replace(" ", "-", $header);
$this->headers[$header] = $v;
}
}
if (array_key_exists("HTTP_REFERER", $_SERVER)) {
$this->referer = $_SERVER['HTTP_REFERER'];
} else {
$this->referer = "";
}
}
public function type() {
return $this->type;
}
public function headers() {
return $this->headers;
}
public function header($h) {
if (array_key_exists($h, $this->headers)) {
return $this->headers[$h];
}
return false;
}
public function file($name, $num = false) {
if ($num === false) {
return $this->files[$name];
}
if ($num >= count($this->files[$name]["name"])) {
return false;
}
return [
"name" => $this->files[$name]["name"][$num],
"full_path" => $this->files[$name]["full_path"][$num],
"type" => $this->files[$name]["type"][$num],
"tmp_name" => $this->files[$name]["tmp_name"][$num],
"error" => $this->files[$name]["error"][$num],
"size" => $this->files[$name]["size"][$num],
];
}
public function get($k) {
if (!array_key_exists($k, $this->get)) return false;
return $this->get[$k];
}
public function post($k) {
if (!array_key_exists($k, $this->post)) return false;
return $this->post[$k];
}
public function put($k) {
if (!array_key_exists($k, $this->put)) return false;
return $this->put[$k];
}
public function set_route($r) {
$this->route = $r;
}
public function route() {
return $this->route;
}
public function path() {
return $this->path;
}
}
Executable
+112
View File
@@ -0,0 +1,112 @@
<?php
class Route {
public $method = "GET";
public $pattern = "";
public $function = null;
public $auth = false;
public $args = [];
public $path = "";
public static $current = false;
function __construct($m, $p, $f, $a = false) {
$this->method = $m;
$this->pattern = $p;
$this->function = $f;
$this->auth = $a;
}
function matches($m, $path) {
if ($this->auth != false) {
if ($this->auth instanceof \Closure) {
if (!$this->auth->call($this)) {
return false;
}
}
if (is_array($this->auth)) {
$c = $this->auth[0];
$f = $this->auth[1];
if (!$c::$f()) {
return false;
}
}
}
if (strtolower($m) != strtolower($this->method)) return false;
$src_parts = explode("/", $path);
$dst_parts = explode("/", $this->pattern);
if (count($src_parts) != count($dst_parts)) {
return false;
}
$this->args = [];
for ($i = 0; $i < count($src_parts); $i++) {
$sp = $src_parts[$i];
$dp = $dst_parts[$i];
if (preg_match('/^{(.*)}$/', $dp, $m)) {
$this->args[$m[1]] = $sp;
continue;
}
if ($sp != $dp) {
return false;
}
}
return true;
}
function get_args() {
return $this->args;
}
function call($req) {
Route::$current = $this;
if ($this->function instanceof \Closure) {
// $fargs = func_get_args($this->function);
// if (in_array("_request", $fargs)) {
// $this->args["_request"] = $req;
// }
$ref = new ReflectionFunction($this->function);
foreach ($ref->getParameters() as $arg) {
if ($arg->name == "_request") {
$this->args["_request"] = $req;
}
}
return $this->function->call($this, ...$this->args);
}
if (is_array($this->function)) {
$c = $this->function[0];
$f = $this->function[1];
$ref = new ReflectionMethod($c, $f);
foreach ($ref->getParameters() as $arg) {
if ($arg->name == "_request") {
$this->args["_request"] = $req;
}
}
return $c::$f(...$this->args);
}
return [404, "Not Found"];
}
public static function canonical() {
$c = Route::$current;
if (!$c->path) {
return Config::get("URL");
}
$bits = parse_url($c->path);
return Config::get("URL") . $bits["path"];
}
}
Executable
+37
View File
@@ -0,0 +1,37 @@
<?php
class Routes {
public static $api = [];
public static $web = [];
static function add_api($method, $path, $function, $auth = false) {
$r = new Route($method, $path, $function, $auth);
Routes::$api[] = $r;
}
static function add_web($method, $path, $function, $auth = false) {
$r = new Route($method, $path, $function, $auth);
Routes::$web[] = $r;
}
static function find_api($m, $path) {
foreach (Routes::$api as $route) {
if ($route->matches($m, $path)) {
return $route;
}
}
return false;
}
static function find_web($m, $path) {
foreach (Routes::$web as $route) {
if ($route->matches($m, $path)) {
$route->path = $path;
return $route;
}
}
return false;
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
class Session {
public static $cache = null;
public static $id = null;
public static function getID() {
if (array_key_exists("SESSION_ID", $_COOKIE)) {
Session::$id = $_COOKIE["SESSION_ID"];
}
if (Session::$id) return Session::$id;
Session::$id = uniqid("decpdf", true);
setcookie("SESSION_ID", Session::$id, time() + 60*60*24*30);
return Session::$id;
}
public static function init($host, $port, $weight = 50) {
if (Session::$cache == null) {
Session::$cache = new Memcached();
}
Session::$cache->addServer($host, $port, $weight);
}
public static function get(string $key) : mixed {
$sid = Session::getID();
$key = "Session::" . $sid . "::" . $key;
$t = Session::$cache->get($key);
if ($t) {
return unserialize($t);
} else {
return false;
}
}
public static function set(string $key, mixed $value) : void {
$sid = Session::getID();
$fkey = "Session::" . $sid . "::" . $key;
Session::$cache->set($fkey, serialize($value));//, 60*60*24*30);
}
public static function unset(string $key) : void {
$sid = Session::getID();
$fkey = "Session::" . $sid . "::" . $key;
Session::$cache->delete($fkey);
}
public static function all_data() {
$sid = Session::getID();
$fkey = "Session::" . $sid . "::";
$keys = Session::$cache->getAllKeys();
$data = [];
foreach ($keys as $k) {
if (str_starts_with($k, $fkey)) {
$d = Session::get($k);
if (!$d) {
$d = [];
}
$out[$k] = $data;
}
}
return $out;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
class Text {
private $text;
public function __construct($text) {
if (!$text) {
$text = "";
}
$this->text = $this->cleanup($text);
}
function cleanup($text) {
$text = str_replace(" ", " ", $text);
$text = str_replace("\t", " ", $text);
$text = str_replace("\r", " ", $text);
$text = str_replace("\n", " ", $text);
$text = str_replace("\"", "", $text);
$text = str_replace(",", " ", $text);
$text = str_replace("", "-", $text);
$text = str_replace("", "-", $text);
$text = str_replace("Ø", "0", $text);
$text = str_replace("~", "-", $text);
$text = str_replace("--", "-", $text);
$text = preg_replace('/\s\s+/', ' ', $text);
return trim($text);
}
public function get_text() {
return $this->text;
}
public function words() {
$text = preg_replace('/([^A-Z\-0-9]+)/', ' ', strtoupper($this->text));
$words = preg_split('/\s+/', $text);
return $words;
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"bootstrap": "^5.3.7",
"css-loader": "^7.1.2",
"expose-loader": "^5.0.1",
"jquery": "^3.7.1",
"jquery-simple-websocket": "github:jbloemendal/jquery-simple-websocket",
"js-image-zoom": "^0.7.0",
"marked": "^16.1.1",
"mini-css-extract-plugin": "^2.9.2",
"popper.js": "^1.16.1",
"style-loader": "^4.0.0",
"vue": "^3.5.17",
"vue-loader": "^17.4.2",
"webpack": "^5.99.9"
},
"devDependencies": {
"webpack-cli": "^6.0.1"
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+3
View File
@@ -0,0 +1,3 @@
<?php
require_once(__DIR__ . "/../app.php");
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

+20
View File
@@ -0,0 +1,20 @@
{
"short_name": "DECPDF",
"name": "DECPDF",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "/d192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/d512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
+77
View File
@@ -0,0 +1,77 @@
<?php
Routes::add_api("GET", "/api/download/{id}", ["DownloadController", "get_download"], ["Auth", "can_upload"]);
Routes::add_api("PUT", "/api/download", ["DownloadController", "start_download"], ["Auth", "can_upload"]);
Routes::add_api("GET", "/api/downloads", ["DownloadController", "api_downloads"], ["Auth", "can_upload"]);
Routes::add_api("PUT", "/api/downloads", ["DownloadController", "api_add_download"], ["Auth", "can_upload"]);
Routes::add_api("POST", "/api/document/dragdrop", ["DocumentController", "api_drag_drop"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/document/{id}", ["DocumentController", "api_get"]);
Routes::add_api("POST", "/api/document/{id}", ["DocumentController", "api_set"]);
Routes::add_api("GET", "/api/document/{id}/products", ["DocumentController", "api_get_products"]);
Routes::add_api("GET", "/api/document/{id}/available_metadata", ["DocumentController", "api_available_metadata"]);
Routes::add_api("GET", "/api/document/{id}/metadata", ["DocumentController", "api_get_metadata"]);
Routes::add_api("PUT", "/api/document/{id}/metadata", ["DocumentController", "api_new_metadata"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/document/{id}/metadata/{metadata}", ["DocumentController", "api_set_metadata"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/document/{id}/metadata/{metadata}", ["DocumentController", "api_delete_metadata"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/product/trash/{id}", ["ProductController", "api_empty_trash"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/product/{list}", ["ProductController", "api_get_list"]);
Routes::add_api("PUT", "/api/product/{id}", ["ProductController", "api_add_child"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/product/{id}", ["ProductController", "api_set"], ["Auth", "can_moderate"]);
routes::add_api("DELETE", "/api/product/{id}", ["ProductController", "api_delete"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/productmru", ["ProductController", "api_get_mru"]);
Routes::add_api("POST", "/api/productmru/{id}", ["ProductController", "api_add_mru"]);
Routes::add_api("POST", "/api/search/product", ["ProductController", "api_search"]);
Routes::add_api("POST", "/api/move/document/{id}", ["DocumentController", "api_move"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/move/product/{id}", ["ProductController", "api_move"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/merge/document/{id}", ["DocumentController", "api_merge"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/revision/{id}", ["RevisionController", "api_set"]);
Routes::add_api("POST", "/api/search/title", ["SearchController", "api_title_search"]);
Routes::add_api("GET", "/api/spider/pdfs", ["SpiderController", "api_pdfs"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/spider/pdf/{id}", ["SpiderController", "api_reject_pdf"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/spider/pdf", ["SpiderController", "api_reject_pdf"], ["Auth", "can_moderate"]);
Routes::add_api("PUT", "/api/spider/pdf/{id}", ["SpiderController", "api_accept_pdf"], ["Auth", "can_moderate"]);
Routes::add_api("PUT", "/api/spider/pdf", ["SpiderController", "api_accept_pdf"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/spider/pages", ["SpiderController", "api_pages"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/spider/page/{id}", ["SpiderController", "api_reject_page"], ["Auth", "can_moderate"]);
Routes::add_api("PUT", "/api/spider/page/{id}", ["SpiderController", "api_accept_page"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/imports", ["ImportController", "api_imports"], ["Auth", "can_upload"]);
Routes::add_api("DELETE", "/api/import/{id}", ["ImportController", "api_delete_import"], ["Auth", "can_upload"]);
Routes::add_api("POST", "/api/import/{id}", ["ImportController", "api_set_import"], ["Auth", "can_upload"]);
Routes::add_api("PUT", "/api/import/{id}", ["ImportController", "api_add_document"], ["Auth", "can_upload"]);
Routes::add_api("GET", "/api/documentbyid/{id}", ["DocumentController", "get_by_id"]);
Routes::add_api("GET", "/api/sys/idmatch", ["SystemController", "api_get_idmatches"], ["Auth", "can_moderate"]);
Routes::add_api("PUT", "/api/sys/idmatch", ["SystemController", "api_add_idmatch"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/sys/idmatch/{id}", ["SystemController", "api_set_idmatch"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/sys/idmatch/{id}", ["SystemController", "api_del_idmatch"], ["Auth", "can_moderate"]);
Routes::add_api("POST", "/api/search/prefix", ["DocumentController", "api_get_title_fragment"]);
Routes::add_api("GET", "/api/product/{id}/available_metadata", ["ProductController", "api_available_metadata"]);
Routes::add_api("PUT", "/api/product/{id}/metadata", ["ProductController", "api_add_metadata"]);
Routes::add_api("GET", "/api/jobs/{source}", ["JobController", "api_get_jobs"], ["Auth", "can_moderate"]);
Routes::add_api("DELETE", "/api/jobs/{id}", ["JobController", "api_delete_job"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/docid/{id}", ["DocumentController", "api_guess_docid"], ["Auth", "can_moderate"]);
Routes::add_api("GET", "/api/product/gemini_all/{id}/{move}", ["ProductController", "api_gemini_all"], ["Auth", "can_moderate"]);
Routes::add_api("PUT", "/api/revision/rate/{id}/{rating}", ["RevisionController", "rate_revision"], ["Auth", "logged_in"]);
+110
View File
@@ -0,0 +1,110 @@
<?php
Routes::add_web("GET", "/", ["Homecontroller", "index"]);
Routes::add_web("GET", "/cover/{id}/{size}/{filename}", ["CoverController", "get_cover"]);
Routes::add_web("GET", "/cover/{id}/{filename}", ["CoverController", "get_cover"]);
Routes::add_web("GET", "/documents/{pid}", ["DocumentController", "browse"]);
Routes::add_web("GET", "/documents", ["DocumentController", "browse"]);
Routes::add_web("GET", "/document/{id}", ["DocumentController", "show"]);
Routes::add_web("GET", "/overview/create/{id}/{move}", ["DocumentController", "create_overview"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/revision/{id}", ["RevisionController", "show"]);
Routes::add_web("GET", "/pdf/{id}/{type}/{filename}", ["PDFController", "download"]);
Routes::add_web("GET", "/page/{id}/{page}/page.jpg", ["PDFController", "get_page"]);
Auth::routes();
Routes::add_web("POST", "/search", ["SearchController", "search"]);
Routes::add_web("GET", "/search", ["SearchController", "search"]);
Routes::add_web("POST", "/search/{page}", ["SearchController", "search"]);
Routes::add_web("GET", "/search/{page}", ["SearchController", "search"]);
Routes::add_web("GET", "/spider_pdfs", ["SpiderController", "spider_pdfs"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/spider_pages", ["SpiderController", "spider_pages"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/status", ["SystemController", "status"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/downloads", ["ImportController", "downloads"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/del_docproduct/{doc}/{prod}", ["DocumentController", "del_docproduct"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/imports", ["ImportController", "imports"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/idmatch", function() { return blade("idmatch"); }, ["Auth", "can_moderate"]);
Routes::add_web("GET", "/merge/{id}", ["DocumentController", "merge"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/explode/{id}", ["DocumentController", "separate"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/test", function($_request) {
print("<pre>");
print_r($_request);
exit(0);
});
Routes::add_web("GET", "/privacy", function() { return blade("privacy"); });
Routes::add_web("GET", "/wordpress/wp-admin/setup-config.php", ["AntiSpam", "tarpit"]);
Routes::add_web("GET", "/wp-admin/setup-config.php", ["AntiSpam", "tarpit"]);
Routes::add_web("HEAD", "/", function() { return "OK"; });
Routes::add_web("GET", "/document/{id}/metadata/{metadata}/delete", ["DocumentController", "delete_metadata"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/delete/revision/{id}", ["RevisionController", "delete"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/redownload/revision/{id}", ["RevisionController", "redownload"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/purge/revision/{id}", ["RevisionController", "purge"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/attachment/{id}/{filename}", ["DocumentController", "download_attachment"]);
Routes::add_web("GET", "/upload", ["DocumentController", "upload_document"], ["Auth", "can_upload"]);
Routes::add_web("POST", "/upload", ["DocumentController", "do_upload_document"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/upload/attachment/{id}", ["DocumentController", "upload_attachment"], ["Auth", "can_upload"]);
Routes::add_web("POST", "/upload/attachment/{id}", ["DocumentController", "do_upload_attachment"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads", ["DocumentController", "uploads"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/import/{id}/{source}", ["DocumentController", "import"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/duplicate/{id}", ["DocumentController", "upload_duplicate"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/rerunall", ["DocumentController", "rerun_all"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/rerun/{id}", ["DocumentController", "rerun_upload"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/discard/{id}", ["DocumentController", "discard_upload"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/uploads/{page}", ["DocumentController", "uploads_page"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/generate/sitemap.xml", ["SitemapController", "generate"]);
Routes::add_web("GET", "/recompress/revision/{id}", ["RevisionController", "recompress"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/ocr/revision/{id}", ["RevisionController", "ocr"], ["Auth", "can_moderate"]);
Routes::add_web("GET", "/uploads/view/{id}", ["DocumentController", "view_upload"], ["Auth", "can_upload"]);
Routes::add_web("GET", "/document/unfavourite/{doc}", ["FavouriteController", "user_unset_favourite"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/document/favourite/{doc}", ["FavouriteController", "user_set_favourite"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/favourites", ["FavouriteController", "favourites"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/systems", ["SystemController", "systems"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/system/add", ["SystemController", "systems_add"], ["Auth", "logged_in"]);
Routes::add_web("POST", "/system/add", ["SystemController", "systems_do_add"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/system/{id}", ["SystemController", "system"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/system/{id}/add/{doc}", ["SystemController", "system_add_doc"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/system/{id}/del/{doc}", ["SystemController", "system_del_doc"], ["Auth", "logged_in"]);
Routes::add_web("GET", "/extract/revision/{id}", ["RevisionController", "split_revision"], ["Auth", "can_moderate"]);
Executable
+24
View File
@@ -0,0 +1,24 @@
<template>
<div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
],
setup() {
return {
}
},
methods: {
},
mounted() {
}
}
</script>
Executable
+90
View File
@@ -0,0 +1,90 @@
<template>
<div :draggable="draggable" :class="dropclass" @dragover="drag_over" @dragstart="drag_start" @drop="drop" @dragenter="drag_enter" @dragleave="drag_leave">
<slot></slot>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"draggable",
"droppable",
"object_id",
"object_type",
"callback",
"extra_data",
],
setup(props, context) {
return {
dropclass: ref(""),
content: ref(""),
}
},
methods: {
init() {
},
drag_over(e) {
e.preventDefault();
},
drag_start(e) {
if (this.draggable == "true") {
e.dataTransfer.setData("object_id", this.object_id);
e.dataTransfer.setData("object_type", this.object_type);
e.dataTransfer.setData("extra_data", this.extra_data);
}
},
drop(e) {
if (this.droppable == "true") {
this.dropclass = "";
e.preventDefault();
console.log(e.dataTransfer.getData("object_id"));
console.log(e.dataTransfer.getData("object_type"));
console.log(e.dataTransfer.getData("extra_data"));
console.log(this);
$.ajax({
url: this.callback,
method: 'POST',
data: {
dst_id: this.object_id,
dst_type: this.object_type,
dst_extra: this.extra_data,
src_id: e.dataTransfer.getData("object_id"),
src_type: e.dataTransfer.getData("object_type"),
src_extra: e.dataTransfer.getData("extra_data"),
copy: e.ctrlKey,
}
}).done(this.drop_complete);
}
},
drop_complete(data) {
//console.log(data);
window.location.reload();
},
drag_enter(e) {
if (this.droppable == "true") {
this.dropclass = "bg-warning-subtle";
}
},
drag_leave(e) {
if (this.droppable == "true") {
this.dropclass = "";
}
},
},
mounted() {
this.init();
}
}
</script>
+195
View File
@@ -0,0 +1,195 @@
<template>
<div class="btn-group">
<button class="btn btn-primary" @click="edit" title="Edit Document"><i class='fa fa-pencil'></i></button>
<div id="docedit" class="modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Document</h5>
<button @click="close" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div v-if="loading" class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else>
<div class="row">
<div class="col-lg-6 col-12">
<label for="internal_id">Order Number</label>
<div class="input-group">
<input @keyup.enter="save" @keyup.esc="close" @keyup="id_caps" type="text" class="form-control" name="internal_id" v-model="internal_id">
<button @click="guess_docid" class="btn btn-secondary" title="Guess document ID"><i class="fa fa-wand-magic-sparkles"></i></button>
</div>
<label for="title">Title</label>
<div class="input-group">
<input ref="rtitle" @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="title" v-model="title">
<button @click="split_title" class="btn btn-secondary" title="Split down on cursor or selection"><i class="fa fa-i-cursor"></i></button>
</div>
<label for="subtitle">Subtitle</label>
<div class="input-group">
<input ref="rsubtitle" @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="subtitle" v-model="subtitle">
<button @click="split_subtitle" class="btn btn-secondary" title="Split down on cursor or selection"><i class="fa fa-i-cursor"></i></button>
</div>
<label for="subsubtitle">Sub-subtitle</label>
<input @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="subsubtitle" v-model="subsubtitle">
</div>
<div class="col-lg-6 col-12">
<label for="overview">Overview <small>(Markdown supported)</small></label>
<textarea @keyup.esc="close" name="overview" v-model="overview" class="form-control w-100" rows="5"></textarea>
<label for="products">Products</label>
<productselector v-model="products"></productselector>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="close" type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button @click="save" type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
setup() {
return {
loading: ref(true),
internal_id: ref(""),
title: ref(""),
subtitle: ref(""),
subsubtitle: ref(""),
overview: ref(""),
products: ref(""),
}
},
mounted() {
},
methods: {
edit() {
this.loading = true;
$('#docedit').show();
$.ajax({
url: "/api/document/" + this.docid,
method: "GET"
}).done(this.get_products);
},
get_products(data) {
this.internal_id = data.internal_id;
this.title = data.title;
this.subtitle = data.subtitle;
this.subsubtitle = data.subsubtitle;
this.overview = data.overview;
$.ajax({
url: "/api/document/" + this.docid + "/products",
method: "GET"
}).done(this.fill_products);
},
fill_products(data) {
this.products = "";
for (var i = 0; i < data.length; i++) {
if (this.products != "") {
this.products += ",";
}
this.products += data[i].id;
}
this.loading = false;
},
close() {
$('#docedit').hide();
},
save() {
$.ajax({
url: "/api/document/" + this.docid,
method: "POST",
data: {
internal_id: this.internal_id,
title: this.title,
subtitle: this.subtitle,
subsubtitle: this.subsubtitle,
overview: this.overview,
products: this.products,
}
}).done(this.refresh);
},
refresh() {
window.location.reload();
},
id_caps() {
this.internal_id = this.internal_id.toUpperCase();
this.internal_id = this.internal_id.replace("", "-");
},
get_caret_position(oField) {
// Initialize
var iCaretPos = 0;
if (document.selection) {
// Set focus on the element
oField.focus();
// To get cursor position, get empty selection range
var oSel = document.selection.createRange();
// Move selection start to 0 position
oSel.moveStart('character', -oField.value.length);
// The caret position is selection length
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionDirection=='backward' ? oField.selectionStart : oField.selectionEnd;
}
return iCaretPos;
},
split_title() {
var pos = this.get_caret_position(this.$refs.rtitle);
var l = this.title.substring(0, pos);
var r = this.title.substring(pos);
this.title = l;
this.subtitle = r;
console.log(pos);
},
split_subtitle() {
var pos = this.get_caret_position(this.$refs.rsubtitle);
var l = this.subtitle.substring(0, pos);
var r = this.subtitle.substring(pos);
this.subtitle = l;
this.subsubtitle = r;
console.log(pos);
},
guess_docid() {
$.ajax({
url: "/api/docid/" + this.docid,
method: "GET"
}).done(this.guessed_docid);
},
guessed_docid(d) {
this.internal_id = d.docid;
},
},
props: [
"docid",
],
}
</script>
+127
View File
@@ -0,0 +1,127 @@
<template>
<div>
<div class="progress {{pstyle}}" role="progressbar" aria-label="Download progress" style="--bs-progress-bar-transition: 0.01s;">
<div ref="pg" :class="pbstyle" :style="{'width': percent + '%'}">{{status}}</div>
</div>
<div class="text-truncate">
<small>{{srcurl}}</small>
</div>
<div v-if="failed" class="alert alert-danger">
The download has failed with error code <strong>{{errorcode}}</strong>. Please try again.
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler'
export default {
emits: [
'complete',
'failure'
],
props: [
'jobid',
'url'
],
setup() {
return {
percent: ref(100),
pbstyle: ref(""),
pstyle: ref(""),
outfile: ref(""),
srcurl: ref(""),
failed: ref(false),
errorcode: ref(""),
status: ref(""),
}
},
methods: {
get_status() {
$.ajax({
url: "/api/download/" + this.jobid,
method: "get",
}) .fail(this.get_status_fail)
.done(this.got_status);
},
get_status_fail(data, status, xhr) {
this.errorcode = xhr;
this.failed = true;
},
got_status(data, status, xhr) {
this.update_bar(data);
if (data.done != data.size) {
setTimeout(this.get_status, 500);
} else {
if (data.size == 0) {
setTimeout(this.get_status, 2000);
}
}
},
submit(url) {
$.ajax({
url: "/api/download",
method: "put",
data: {
url: this.url
}
}) .fail(this.submit_fail)
.done(this.submitted);
},
submit_fail(data, status, xhr) {
this.errorcode = xhr;
this.failed = true;
this.$emit("failed");
},
submitted(data, status, xhr) {
this.update_bar(data);
setTimeout(this.get_status, 500);
},
update_bar(data) {
this.outfile = data.file;
this.srcurl = data.url;
if (data.started == 0) {
this.status = "Queued";
this.pbstyle = "progress-bar bg-secondary progress-bar-striped progress-bar-animated";
this.percent = 100;
return;
}
if (data.finished != 0) {
this.status = "Completed";
this.pbstyle = "progress-bar bg-success";
this.percent = 100;
this.$emit("complete");
let event = new CustomEvent("complete", { bubbles: true, detail: data });
document.dispatchEvent(event);
return;
}
if (data.size == 0) {
this.status = "Starting...";
} else {
this.percent = data.done / data.size * 100;
this.pbstyle = "progress-bar bg-primary";
this.status = Math.round(this.percent) + "%";
}
}
},
mounted() {
this.status = "Please wait...";
this.pbstyle = "progress-bar bg-secondary progress-bar-striped progress-bar-animated";
this.percent = 100;
if (this.jobid != "") {
this.get_status();
} else if (this.url != "") {
this.submit(url);
}
}
};
</script>
+76
View File
@@ -0,0 +1,76 @@
<template>
<div>
<div class="input-group">
<span class="input-group-text">Download PDF URL:</span>
<input type="text" class="form-control" v-model="url">
<button class="btn btn-primary" @click="add_download">Download</button>
</div>
<ul v-for="download in downloads" class="list-group">
<li class="list-group-item">
<div class="row">
<div class="col-12 text-truncate">
{{ download.url }}
</div>
<div class="col-3">
<download :jobid="download.id"></download>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
],
setup() {
return {
downloads: ref([]),
busy: ref(false),
url: ref(""),
}
},
methods: {
init() {
this.update();
},
update() {
this.busy = true;
$.ajax({
url: "/api/downloads",
method: "GET"
}).done(this.got_update);
},
add_download() {
this.busy = true;
$.ajax({
url: "/api/downloads",
method: "PUT",
data: {
url: this.url
}
}).done(this.got_update);
},
got_update(data) {
this.downloads = data;
this.busy = false;
setTimeout(this.update, 10000);
},
},
mounted() {
this.init();
}
}
</script>
+249
View File
@@ -0,0 +1,249 @@
<template>
<div>
<div class="input-group">
<span class="input-group-text">Test String:</span>
<input @keyup="refresh" class="form-control" type="text" v-model="teststring">
<span class="input-group-text"><i class="fa fa-arrow-left mx-1"></i> Type here to test matches</span>
<button class="btn btn-success" @click="addnew">Add New Match</button>
</div>
<table class="table table-hover w-100">
<thead>
<tr>
<th>Weight</th>
<th>Regular Expression</th>
<th>Example</th>
<th>Test Result</th>
</tr>
</thead>
<tbody>
<tr v-for="match in matches" :class="matchtest(match.regex)" @click="edit_reg(match)">
<td>{{ match.weight }}</td>
<td>/^{{ match.regex }}$/</td>
<td>{{ match.example }}</td>
<td>{{ teststring != "" ? matchresult(match.regex, teststring) : matchresult(match.regex, match.example)}}</td>
</tr>
</tbody>
</table>
<div id="newreg" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New ID Match</h5>
<button @click="addclose" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" :disabled="saving"></button>
</div>
<div class="modal-body">
<label for="example">Example ID</label>
<input type="text" name="example" class="form-control" v-model="add_example" @keyup="do_add_test">
<label for="regex">Regular Expression</label>
<div class="input-group">
<span class="input-group-text">/^</span>
<input type="text" name="regex" class="form-control" v-model="add_regex" @keyup="do_add_test">
<span class="input-group-text">$/</span>
</div>
<label for="weight">Weight (lower is higher priority)</label>
<input type="text" name="weight" class="form-control" v-model="add_weight">
Test Result: {{ add_test }}
</div>
<div class="modal-footer">
<button @click="addclose" type="button" class="btn btn-secondary" data-bs-dismiss="modal" :disabled="saving">Close</button>
<button @click="addsave" type="button" class="btn btn-primary" :disabled="saving">
<span v-if="!saving">Save changes</span>
<span class="spinner-border spinner-border-sm" role="status" v-if="saving"></span>
<span v-if="saving" class="mx-1">Saving...</span>
</button>
</div>
</div>
</div>
</div>
<div id="editreg" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit ID Match</h5>
<button @click="edit_close" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" :disabled="saving"></button>
</div>
<div class="modal-body">
<label for="example">Example ID</label>
<input type="text" name="example" class="form-control" v-model="edit_example" @keyup="do_edit_test">
<label for="regex">Regular Expression</label>
<div class="input-group">
<span class="input-group-text">/^</span>
<input type="text" name="regex" class="form-control" v-model="edit_regex" @keyup="do_edit_test">
<span class="input-group-text">$/</span>
</div>
<label for="weight">Weight (lower is higher priority)</label>
<input type="text" name="weight" class="form-control" v-model="edit_weight">
Test Result: {{ edit_test }}
</div>
<div class="modal-footer">
<button @click="edit_delete" type="button" class="btn btn-danger" :disabled="saving">Delete</button>
<button @click="edit_close" type="button" class="btn btn-secondary" data-bs-dismiss="modal" :disabled="saving">Close</button>
<button @click="edit_save" type="button" class="btn btn-primary" :disabled="saving">
<span v-if="!saving">Save changes</span>
<span class="spinner-border spinner-border-sm" role="status" v-if="saving"></span>
<span v-if="saving" class="mx-1">Saving...</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
],
setup() {
return {
matches: ref([]),
teststring: ref(""),
saving: ref(false),
add_example: ref(""),
add_regex: ref(""),
add_weight: ref(0),
add_test: ref(""),
edit_example: ref(""),
edit_regex: ref(""),
edit_weight: ref(0),
edit_test: ref(""),
edit_id: ref(0),
}
},
methods: {
init() {
this.update();
},
refresh() {
var m = this.matches;
this.matches = [];
this.matches = m;
},
update() {
$.ajax({
url: "/api/sys/idmatch",
method: "GET"
}).done(this.got_update);
},
got_update(data) {
this.matches = data;
},
matchresult(regex, example) {
try {
const re = new RegExp("^" + regex + "$");
var res = example.match(re);
if (res == null) {
return "FAILED";
}
return res[1];
} catch (E) {
return "Bad Syntax";
}
},
matchtest(regex) {
var teststring = this.teststring.trim();
if (teststring == "") return "";
if (this.matchresult(regex, teststring) != "FAILED") {
return "table-success";
}
return "table-danger";
},
addnew() {
this.add_example = "";
this.add_regex = "";
this.add_weight = 0;
$('#newreg').show();
},
do_add_test() {
this.add_test = this.matchresult(this.add_regex, this.add_example);
},
addclose() {
$('#newreg').hide();
},
addsave() {
$.ajax({
url: "/api/sys/idmatch",
method: "PUT",
data: {
example: this.add_example,
regex: this.add_regex,
weight: this.add_weight
}
}).done(this.got_update);
$('#newreg').hide();
},
edit_reg(m) {
this.edit_id = m.id;
this.edit_example = m.example;
this.edit_regex = m.regex;
this.edit_weight = m.weight;
$('#editreg').show();
},
edit_close() {
$('#editreg').hide();
},
do_edit_test() {
this.edit_test = this.matchresult(this.edit_regex, this.edit_example);
},
edit_save() {
$.ajax({
url: "/api/sys/idmatch/" + this.edit_id,
method: "POST",
data: {
example: this.edit_example,
regex: this.edit_regex,
weight: this.edit_weight
}
}).done(this.got_update);
$('#editreg').hide();
},
edit_delete() {
$.ajax({
url: "/api/sys/idmatch/" + this.edit_id,
method: "DELETE",
}).done(this.got_update);
$('#editreg').hide();
},
},
mounted() {
this.init();
}
}
</script>
+550
View File
@@ -0,0 +1,550 @@
<template>
<div>
<ul class="list-group">
<li v-for="i in imports" :class="fancyrow(i)">
<div class="row">
<div class="col-1">
<a v-if="i.revision != null" :href='"/cover/" + i.revision.id + "/cover.jpg"'><img class="w-100" :src='"/cover/" + i.revision.id + "/100/cover.jpg"'></a>
</div>
<div class="col-11">
<div class="row">
<div class="col-lg-2 col-6">
<strong v-if="i.imported > 0">Imported</strong>
<strong v-else-if="i.completed > 0 && i.revision">Completed</strong>
<strong v-else-if="i.completed > 0 && (!i.revision)">Failed</strong>
<strong v-else-if="i.started > 0">Processing</strong>
<strong v-else>Queued</strong>
</div>
<div class="col-lg-2 col-6">
<a v-if="i.revision != null" :href='"/revision/" + i.revision.id'>{{ i.revision.id }}</a>
</div>
<div class="col-lg-2 col-3">
{{ i.guessed_id }}
</div>
<div class="col-lg-2 col-3">
{{ i.guessed_revno }}
</div>
<div class="col-lg-2 col-6">
{{ i.guessed_month }}/{{ i.guessed_year }}
</div>
</div>
<div class="row">
<div class="col-9 text-truncate">
{{ i.guessed_title }}
</div>
<div class="col-lg-3 col-12 btn-group">
<button :disabled="busy" class="btn btn-danger" @click="del(i)"><i class="fa-solid fa-trash"></i></button>
<button :disabled="busy || (i.revision == null)" class="btn btn-secondary" @click="download(i)"><i class="fa-solid fa-download"></i></button>
<button :disabled="(i.started == 0) || busy" class="btn btn-primary" @click="restart(i)"><i class="fa-solid fa-rotate"></i></button>
<button class="btn btn-success" :disabled="(i.completed == 0 || (!i.revision)) || busy" @click="import_rev(i)"><i class="fa-solid fa-file-import"></i></button>
</div>
</div>
<div class="row">
<div class="col-12">
{{ i.origfile }}
</div>
</div>
</div>
</div>
</li>
</ul>
<div id="docedit" class="modal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create Document</h5>
<button @click="close" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" :disabled="saving"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-12 alert alert-info">
{{ i_origfile }}
</div>
</div>
<pdf :rev="i_revision"></pdf>
<div class="row">
<div class="col-lg-6 col-12">
<label for="i_id">Order Number</label>
<div class="input-group">
<input @blur="upper_id" @keyup.enter="save" ref="orderno" @keyup.esc="close" type="text" @keyup="find_doc" :class="'form-control' + ((i_id.length <= 25) ? ' is-valid' : ' is-invalid')" name="i_id" v-model="i_id" :disabled="saving">
<button class="btn btn-secondary" @click="paste_id"><i class="fa-solid fa-paste"></i></button>
</div>
<div v-if="newdoc" class="alert alert-warning">
A new document will be created with this order number.
</div>
<div v-else class="alert alert-success">
This PDF will be added to {{ existing_doc.title }} {{ existing_doc.subtitle }} {{ existing_doc.subsubtitle }} as a new revision.
</div>
<div v-if="newdoc">
<label for="i_title">Title</label>
<div class="input-group">
<input ref="rtitle" @keyup.enter="save" @keyup.esc="close" @keyup="try_title" type="text" :class="'form-control' + (i_title == '' ? ' is-invalid' : ' is-valid')" name="i_title" v-model="i_title" :disabled="saving">
<button class="btn btn-secondary" @click="split_title" title="Split down on cursor position"><i class='fa fa-i-cursor'></i></button>
</div>
<label for="i_subtitle">Subtitle</label>
<div class="input-group">
<input ref="rsubtitle" @keyup.enter="save" @keyup.esc="close" @keyup="try_subtitle" type="text" class="form-control" name="i_subtitle" v-model="i_subtitle" :disabled="saving">
<button class="btn btn-secondary" @click="split_subtitle" title="Split down on cursor position"><i class='fa fa-i-cursor'></i></button>
</div>
<label for="i_subsubtitle">Sub-subtitle</label>
<input @keyup.enter="save" @keyup.esc="close" @keyup="try_subsubtitle" type="text" class="form-control" name="i_subsubtitle" v-model="i_subsubtitle" :disabled="saving">
</div>
</div>
<div class="col-lg-6 col-12">
<label for="i_revno">Revision / Version Number</label>
<input @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="i_revno" v-model="i_revno" :disabled="saving">
<div class="row">
<div class="col-6">
<label for="i_month">Month</label>
<input @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="i_month" v-model="i_month" :disabled="saving">
</div>
<div class="col-6">
<label for="i_year">Year</label>
<input @keyup.enter="save" @keyup.esc="close" type="text" class="form-control" name="i_year" v-model="i_year" :disabled="saving">
</div>
</div>
<div v-if="newdoc">
<label for="i_overview">Overview <small>(Markdown supported)</small></label>
<textarea @keyup.esc="close" name="i_overview" v-model="i_overview" class="form-control w-100" rows="5" :disabled="saving"></textarea>
<label for="i_products">Products</label>
<productselector id="ps" v-model="i_products"></productselector>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<textarea editable="false" v-model="i_covertext" class="form-control"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="close" type="button" class="btn btn-secondary" data-bs-dismiss="modal" :disabled="saving">Close</button>
<button @click="save" type="button" class="btn btn-primary" :disabled="saving || (newdoc && ((i_products == '') || (i_title == '')))">
<span v-if="!saving">Save changes</span>
<span class="spinner-border spinner-border-sm" role="status" v-if="saving"></span>
<span v-if="saving" class="mx-1">Saving...</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
],
setup() {
return {
imports: ref([]),
busy: ref(false),
i_products: ref(""),
i_id: ref(""),
i_title: ref(""),
i_subtitle: ref(""),
i_subsubtitle: ref(""),
i_revno: ref(""),
i_month: ref(0),
i_year: ref(0),
i_overview: ref(""),
i_cover: ref(""),
i_fullcover: ref(""),
i_origfile: ref(""),
i_covertext: ref(""),
i_revision: ref(0),
triglock: ref(false),
saving: ref(false),
importing: ref({
id: 0
}),
newdoc: ref(true),
existing_doc: ref({
title: '',
subtitle: '',
subsubtitle: '',
}),
timer: ref(null),
zoom: ref(null),
fsel_start: ref(0),
fsel_end: ref(0),
fsel_el: ref(null),
keytimer : ref(false),
}
},
methods: {
init() {
this.update();
var that = this;
},
focus() {
this.$refs.rtitle.focus();
this.zoom = new ImageZoom(document.getElementById("cover"), {
fillContainer: true,
offset: { vertical: 0, horizontal: 10 },
zoomStyle: "z-index: 10;",
});
},
update() {
this.timer = false;
$.ajax({
url: "/api/imports",
method: "GET"
}).done(this.got_update);
},
got_update(data) {
this.imports = data;
this.busy = false;
var r = 0;
for (var i = 0; i < this.imports.length; i++) {
if (this.imports[i].completed == 0) {
r = 1;
}
}
if (r) {
if (this.timer) {
clearTimeout(this.timer);
this.timer=null;
}
this.timer = setTimeout(this.update, 1000);
}
},
del(rev) {
this.busy = true;
$.ajax({
url: "/api/import/" + rev.id,
method: "DELETE"
}).done(this.got_update);
},
restart(rev) {
this.busy = true;
$.ajax({
url: "/api/import/" + rev.id,
method: "POST",
data: {
started: 0,
completed: 0,
imported: 0,
}
}).done(this.got_update);
},
import_rev(rev) {
this.importing = rev;
this.i_id = rev.guessed_id;
this.i_title = rev.guessed_title;
this.i_revno = rev.guessed_revno;
this.i_subtitle = "";
this.i_subsubtitle = "";
this.i_month = rev.guessed_month;
this.i_year = rev.guessed_year;
this.i_overview = rev.overview;
this.i_products = "";
this.i_cover = "/cover/" + rev.revision.id + "/cover.jpg";
this.i_fullcover = "/cover/" + rev.revision.id + "/cover.jpg";
this.i_origfile = rev.origfile;
this.i_covertext = rev.covertext;
this.i_revision = rev.revision.id;
this.find_doc();
$(document).on('hide.bs.modal', '#docedit', function(e) {alert("doc"); });
$('#docedit').on('hide.bs.modal', function(e) {alert("modal"); });
$('#docedit').show();
setTimeout(this.focus, 10);
},
test(e) {
console.log(e);
},
close() {
$('#docedit').hide();
},
save() {
if (this.busy) return;
if (this.saving) return;
if (this.newdoc && (this.i_products == '')) return;
if (this.newdoc && (this.i_title == '')) return;
this.busy = true;
this.saving = true;
$.ajax({
url: "/api/import/" + this.importing.id ,
method: "PUT",
data: {
internal_id: this.i_id,
title: this.i_title,
subtitle: this.i_subtitle,
subsubtitle: this.i_subsubtitle,
month: this.i_month,
year: this.i_year,
overview: this.i_overview,
products: this.i_products,
revno: this.i_revno,
}
}).done(this.saved);
},
saved(data) {
this.got_update(data);
this.saving = false;
$('#docedit').hide();
delete this.zoom;
},
find_doc() {
var i_id = this.i_id.toUpperCase();
$.ajax({
url: "/api/documentbyid/" + i_id,
method: "GET"
}).done(this.got_doc);
},
got_doc(data) {
if (data == false) {
this.newdoc = true;
} else {
this.newdoc = false;
this.existing_doc = data;
}
},
fancyrow(i) {
if (i.completed > 0) {
if (!i.revision) {
return "list-group-item list-group-item-danger";
} else {
return "list-group-item list-group-item-success";
}
}
if (i.started > 0) return "list-group-item list-group-item-warning";
if (i.queued > 0) return "list-group-item list-group-item-primary";
return "list-group-item";
},
get_caret_position(oField) {
// Initialize
var iCaretPos = 0;
if (document.selection) {
// Set focus on the element
oField.focus();
// To get cursor position, get empty selection range
var oSel = document.selection.createRange();
// Move selection start to 0 position
oSel.moveStart('character', -oField.value.length);
// The caret position is selection length
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionDirection=='backward' ? oField.selectionStart : oField.selectionEnd;
}
return iCaretPos;
},
select_to_end(oField, from) {
if (document.selection) {
oField.focus();
var oSel = document.selection.createRange();
oSel.moveStart('character', from);
oSel.moveEnd('character', oField.value.length);
} else {
oField.setSelectionRange(from, oField.value.length);
}
},
split_title() {
var pos = this.get_caret_position(this.$refs.rtitle);
var l = this.i_title.substring(0, pos);
var r = this.i_title.substring(pos);
this.i_title = l;
this.i_subtitle = r;
},
split_subtitle() {
var pos = this.get_caret_position(this.$refs.rsubtitle);
var l = this.i_subtitle.substring(0, pos);
var r = this.i_subtitle.substring(pos);
this.i_subtitle = l;
this.i_subsubtitle = r;
},
upper_id() {
this.i_id = this.i_id.toUpperCase();
},
paste_id() {
if (navigator.clipboard) {
navigator.clipboard.readText().then(this.do_set_id);
}
},
do_set_id(data) {
this.i_id = data;
this.find_doc();
},
download(data) {
document.location="/pdf/" + data.revision.id + "/download/doc.pdf";
},
try_title(e) {
if (e.key == "Shift") return;
if (e.key == "ArrowLeft") return;
if (e.key == "ArrowRight") return;
if (e.key == "ArrowUp") return;
if (e.key == "ArrowDown") return;
if (e.key == "Backspace") return;
if (this.triglock) return;
if (this.keytimer) {
clearTimeout(this.keytimer);
}
this.keytimer = setTimeout(this.try_title2, 250);
},
try_title2() {
this.keytimer = false
$.ajax({
url: "/api/search/prefix",
method: "POST",
data: {
title: this.i_title
}
}).done(this.fill_title);
},
fill_title(data, status) {
if (status == "success") {
this.triglock = true;
this.fsel_start = this.get_caret_position(this.$refs.rtitle);
this.fsel_el = this.$refs.rtitle;
this.i_title = data;
this.triglock = false;
setTimeout(this.fsel, 10);
}
},
try_subtitle(e) {
if (e.key == "Shift") return;
if (e.key == "ArrowLeft") return;
if (e.key == "ArrowRight") return;
if (e.key == "ArrowUp") return;
if (e.key == "ArrowDown") return;
if (e.key == "Backspace") return;
if (this.triglock) return;
if (this.keytimer) {
clearTimeout(this.keytimer);
}
this.keytimer = setTimeout(this.try_subtitle2, 250);
},
try_subtitle2() {
this.keytimer = false
$.ajax({
url: "/api/search/prefix",
method: "POST",
data: {
title: this.i_subtitle
}
}).done(this.fill_subtitle);
},
fill_subtitle(data, status) {
if (status == "success") {
this.triglock = true;
this.fsel_start = this.get_caret_position(this.$refs.rsubtitle);
this.fsel_el = this.$refs.rsubtitle;
this.i_subtitle = data;
this.triglock = false;
setTimeout(this.fsel, 10);
}
},
try_subsubtitle(e) {
if (e.key == "Shift") return;
if (e.key == "ArrowLeft") return;
if (e.key == "ArrowRight") return;
if (e.key == "ArrowUp") return;
if (e.key == "ArrowDown") return;
if (e.key == "Backspace") return;
if (this.triglock) return;
if (this.keytimer) {
clearTimeout(this.keytimer);
}
this.keytimer = setTimeout(this.try_subsubtitle2, 250);
},
try_subsubtitle2() {
this.keytimer = false
$.ajax({
url: "/api/search/prefix",
method: "POST",
data: {
title: this.i_subsubtitle
}
}).done(this.fill_subsubtitle);
},
fill_subsubtitle(data, status) {
if (status == "success") {
this.triglock = true;
this.fsel_start = this.get_caret_position(this.$refs.rsubsubtitle);
this.fsel_el = this.$refs.rsubsubtitle;
this.i_subsubtitle = data;
this.triglock = false;
setTimeout(this.fsel, 10);
}
},
fsel() {
this.select_to_end(this.fsel_el, this.fsel_start);
},
},
mounted() {
this.init();
}
}
</script>
+85
View File
@@ -0,0 +1,85 @@
<template>
<div>
<div v-if="editing" class="input-group">
<input
ref="edval"
class="form-control"
type="text"
v-model="text"
@keyup.enter="submit_edit"
@keyup.esc="cancel_edit"
>
<button @click="cancel_edit" class="btn btn-danger px-1 py-0">
<i class="fa fa-close"></i>
</button>
<button @click="submit_edit" class="btn btn-success px-1 py-0">
<i class="fa fa-check"></i>
</button>
</div>
<div v-else class="d-flex justify-content-between align-middle">
<span class="align-middle my-auto">{{ text }}</span>
<button @click="enable_edit" class="btn btn-secondary px-1 py-0">
<i class="fa fa-pencil"></i>
</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"value",
"url",
"field",
],
setup() {
return {
editing: ref(false),
text: ref(""),
saved: ref(""),
}
},
methods: {
enable_edit() {
this.editing = true;
this.saved = this.text;
var that = this;
setTimeout(function() { $(that.$refs.edval).focus() }, 100);
},
cancel_edit() {
this.text = this.saved;
this.editing = false;
},
submit_edit() {
var data = {};
data[this.field] = this.text;
console.log(data);
$.ajax({
url: this.url,
method: "POST",
data: data
});
this.editing = false;
},
},
mounted() {
this.text = this.value;
}
}
</script>
+114
View File
@@ -0,0 +1,114 @@
<template>
<div>
<div v-if="editing">
<textarea
ref="edval"
class="form-control w-100"
rows="5"
type="text"
v-model="text"
@keyup.esc="cancel_edit"
></textarea>
<button @click="cancel_edit" class="btn btn-danger">
<i class="fa fa-close"></i>
</button>
<button @click="submit_edit" class="btn btn-success">
<i class="fa fa-check"></i>
</button>
</div>
<div v-else>
<div class="align-middle my-auto" v-html="md()"></div>
<button @click="enable_edit" class="btn btn-secondary">
<i class="fa fa-pencil"></i>
</button>
</div>
<div ref="slotData" class="visually-hidden">
<slot></slot>
</div>
</div>
</template>
<script>
import { useSlots, ref } from 'vue/dist/vue.esm-bundler';
import { marked } from 'marked';
export default {
props: [
"url",
"field",
],
setup() {
return {
editing: ref(false),
text: ref(""),
saved: ref(""),
}
},
methods: {
init() {
this.load();
},
enable_edit() {
this.editing = true;
this.saved = this.text;
var that = this;
setTimeout(function() { $(that.$refs.edval).focus() }, 100);
},
cancel_edit() {
this.text = this.saved;
this.editing = false;
},
submit_edit() {
var data = {};
data[this.field] = this.text;
$.ajax({
url: this.url,
method: "POST",
data: data
});
this.editing = false;
},
load() {
$.ajax({
url: this.url,
method: "GET"
}).done(this.loaded);
},
loaded(data) {
this.text = data[0][this.field];
},
md() {
var text = this.text;
if (text == null) {
text = "Loading...";
}
var txt = marked.parse(text);
return txt;
},
},
mounted() {
// var div = document.createElement("div");
// div.innerHTML = this.$slots.default()[0].children;
// this.text = div.textContent;
this.init();
}
}
</script>
+87
View File
@@ -0,0 +1,87 @@
<template>
<div>
<div v-if="editing" class="input-group">
<select class="form-control" v-model="sel">
<option v-for='i in items' :value='i.key'>{{ i.value }}</option>
</select>
<button @click="cancel_edit" class="btn btn-danger">
<i class="fa fa-close"></i>
</button>
<button @click="submit_edit" class="btn btn-success">
<i class="fa fa-check"></i>
</button>
</div>
<div v-else>
<button @click="enable_edit" class="btn btn-secondary">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"list",
"target",
"method",
],
setup() {
return {
editing: ref(false),
sel: ref(""),
items: ref([]),
}
},
methods: {
enable_edit() {
$.ajax({
url: this.list,
method: "GET"
}).done(this.start_edit);
},
start_edit(d) {
this.items = d;
this.editing = true;
var that = this;
},
cancel_edit() {
this.text = this.saved;
this.editing = false;
},
submit_edit() {
var data = {};
data["item_id"] = this.sel;
$.ajax({
url: this.target,
method: this.method,
data: data
}).done(this.fin);
this.editing = false;
},
fin() {
location.reload();
},
},
mounted() {
this.text = this.value;
}
}
</script>
+83
View File
@@ -0,0 +1,83 @@
<template>
<div>
<ul class="list-group">
<li v-for="job in jobs" :class="'d-flex justify-content-between list-group-item ' + get_group_color(job)">
<span>
{{ job.id }} - {{ job.class }} - {{ job.status }}
</span>
<small v-if='(job.finished + job.failed) > 0'><button @click='delete_job(job)' class='px-1 py-0 btn btn-danger'><i class='fa fa-close'></i></button></small>
</li>
</ul>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"source"
],
setup() {
return {
jobs: ref([]),
speed: ref(1000),
timeout: ref(null),
}
},
methods: {
init() {
this.update();
},
update() {
$.ajax({
url: "/api/jobs/" + this.source,
method: "GET"
}).done(this.updated);
},
updated(d) {
this.refresh(d);
setTimeout(this.update, this.speed);
},
refresh(d) {
this.jobs = d;
this.speed = 10000;
for (var i = 0; i < this.jobs.length; i++) {
if ((this.jobs[i].finished == 0) && (this.jobs[i].failed == 0)) {
this.speed = 1000;
}
}
},
get_group_color(j) {
if (j.failed > 0) {
return "list-group-item-danger";
}
if (j.finished > 0) {
return "list-group-item-success";
}
if (j.started > 0) {
return "list-group-item-warning";
}
return "list-group-item-info";
},
delete_job(j) {
$.ajax({
url: "/api/jobs/" + j.id,
method: "DELETE"
}).done(this.refresh);
},
},
mounted() {
this.init();
}
}
</script>
+34
View File
@@ -0,0 +1,34 @@
<template>
<div style="display: none;">
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"keyname",
"target",
],
setup() {
return {
}
},
methods: {
handler(e) {
console.log(e.key);
if (e.key == this.keyname) {
document.location=this.target;
}
},
},
mounted() {
window.addEventListener('keypress', this.handler);
}
}
</script>
Executable
+89
View File
@@ -0,0 +1,89 @@
<template>
<div>
<div class="row m-3 bg-white">
<div class="col-6 m-0 p-0">
<img @click="nav_left" :class="leftclass()" :src="left">
</div>
<div class="col-6 m-0 p-0">
<img @click="nav_right" :class="rightclass()" :src="right">
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: {
rev: ref(""),
pages: ref(0)
},
setup(props, emit) {
return {
left: ref(""),
right: ref(""),
leftpage : ref(0),
}
},
methods: {
init() {
this.leftpage = 0;
this.update();
this.$watch('rev', this.show);
},
update() {
this.left = "/page/" + this.rev + "/" + this.leftpage + "/page.jpg";
this.right = "/page/" + this.rev + "/" + (this.leftpage + 1) + "/page.jpg";
},
leftclass() {
if (this.leftpage > 0) {
return 'page-left w-100 m-0 p-0';
} else {
return 'page-end w-100 m-0 p-0';
}
},
rightclass() {
if ((this.leftpage + 1) >= this.pages) {
return "invisible w-100 m-0 p-0";
}
if (this.leftpage < this.pages - 2) {
return 'page-right w-100 m-0 p-0';
} else {
return 'page-end w-100 m-0 p-0';
}
},
show() {
this.left = '';
this.right = '';
this.leftpage = 0;
this.update();
},
nav_right() {
if (this.leftpage < this.pages - 2) {
this.leftpage += 2;
}
this.update();
},
nav_left() {
this.leftpage -= 2;
if (this.leftpage < 0) {
this.leftpage = 0;
}
this.update();
},
},
mounted() {
this.init();
}
}
</script>
+123
View File
@@ -0,0 +1,123 @@
<template>
<div>
<ul class="list-group">
<li v-for="pdf in pdfs" class="list-group-item">
<div class="row">
<div class="col-lg-11 col-12">
<div class="row">
<div class="col-12 text-center">
{{ pdf.title }}
</div>
<div class="col-12">
<a class="btn w-100 text-start" :href="pdf.url" target="_blank">{{ pdf.url }}</a>
</div>
</div>
</div>
<div class="btn-group col-lg-1 col-12 text-center">
<button :disabled="busy" @click="reject(pdf)" class="btn btn-danger"><i class="fa fa-close"></i></button>
<button :disabled="busy" @click="accept(pdf)" class="btn btn-success"><i class="fa fa-check"></i></button>
</div>
</div>
</li>
<li class="list-group-item">
<div class="row">
<div class="col-lg-11 col-12 text-center">
<a class="btn w-100" target="_blank">Entire List</a>
</div>
<div class="btn-group col-lg-1 col-12">
<button :disabled="busy" @click="reject_all" class="btn btn-danger"><i class="fa fa-close"></i></button>
<button :disabled="busy" @click="accept_all" class="btn btn-success"><i class="fa fa-check"></i></button>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
],
setup() {
return {
pdfs: ref([]),
busy: ref(false),
}
},
methods: {
init() {
this.update();
},
update() {
this.busy = true;
$.ajax({
url: "/api/spider/pdfs",
method: "GET"
}).done(this.got_update);
},
got_update(data) {
this.pdfs = data;
this.busy = false;
},
accept(pdf) {
if (this.busy) return;
this.busy = true;
$.ajax({
url: '/api/spider/pdf/' + pdf.id,
method: "PUT"
}).done(this.got_update);
},
reject(pdf) {
if (this.busy) return;
this.busy = true;
$.ajax({
url: '/api/spider/pdf/' + pdf.id,
method: "DELETE"
}).done(this.got_update);
},
accept_all() {
if (this.busy) return;
this.busy = true;
var l = [];
for (var i = 0; i < this.pdfs.length; i++) {
l.push(this.pdfs[i].id);
}
$.ajax({
url: '/api/spider/pdf/' + l.join(","),
method: "PUT"
}).done(this.got_update);
},
reject_all() {
if (this.busy) return;
this.busy = true;
var l = [];
for (var i = 0; i < this.pdfs.length; i++) {
l.push(this.pdfs[i].id);
}
$.ajax({
url: '/api/spider/pdf/' + l.join(","),
method: "DELETE"
}).done(this.got_update);
},
},
mounted() {
this.init();
}
}
</script>
+188
View File
@@ -0,0 +1,188 @@
<template>
<nav class="d-flex justify-content-end">
<itemadder :list="'/api/product/' + pid + '/available_metadata'" :target="'/api/product/' + pid + '/metadata'" method="PUT"></itemadder>
<div ref="ddcreate_child" class="dropdown ms-1">
<button class="btn btn-success dropdown-toggle" title="Add" data-bs-toggle="dropdown"><i class="fa fa-plus-circle"></i></button>
<ul class="dropdown-menu" style="min-width: 300px;">
<li class="p-2">
<strong>Create new child product</strong>
<div class="input-group">
<span class="input-group-text">Name</span>
<input @keyup.enter="create_child" ref="increate_child" class="form-control" name="title" v-model="newtitle">
<button class="btn btn-primary" @click="create_child">Create</button>
</div>
</li>
</ul>
</div>
<div ref="ddrename" class="dropdown ms-1">
<button class="btn btn-success dropdown-toggle" title="Rename" data-bs-toggle="dropdown" @click="set_edittitle"><i class="fa fa-pencil"></i></button>
<ul class="dropdown-menu" style="min-width: 300px;">
<li class="p-2">
<strong>Rename product</strong>
<div class="input-group">
<span class="input-group-text">Name</span>
<input ref="inrename" class="form-control" name="title" v-model="edittitle">
<button class="btn btn-primary" @click="rename">Rename</button>
</div>
</li>
</ul>
</div>
<button class="btn btn-success ms-1" @click="gemini_all"><i class="fa-solid fa-wand-magic"></i></button>
<button class="btn btn-success ms-1" @click="gemini_all_move"><i class="fa-solid fa-wand-magic-sparkles"></i></button>
<button :class="'btn ms-1 ' + dp_class" @click="delete_product"><i class="fa fa-trash"></i></button>
<button @click="empty_trash" class="ms-1 btn btn-danger" v-if="prod.title=='Trash'"><i class="fa fa-recycle"></i></button>
</nav>
</template>
<script>
import { ref } from 'vue/dist/vue.esm-bundler';
export default {
props: [
"pid",
],
setup() {
return {
newtitle: ref(""),
edittitle: ref(""),
dp_class: ref("btn-danger"),
dp_clicks: ref(0),
prod: ref({}),
}
},
methods: {
init() {
$(this.$refs.ddcreate_child).on("show.bs.dropdown", this.add_child_show);
$(this.$refs.ddcreate_child).on("shown.bs.dropdown", this.add_child_shown);
$(this.$refs.ddrename).on("show.bs.dropdown", this.rename_show);
$(this.$refs.ddrename).on("shown.bs.dropdown", this.rename_shown);
this.get_product();
},
get_product() {
$.ajax({
url: "/api/product/" + this.pid,
method: "GET"
}).done(this.got_product);
},
add_child_show(e) {
this.newtitle = "";
},
add_child_shown(e) {
$(this.$refs.increate_child).focus();
},
rename_show(e) {
this.edittitle = this.prod.title;
},
set_edittitle() {
this.edittitle = this.prod.title;
},
got_product(data) {
this.prod = data[0];
},
rename_shown(e) {
},
create_child() {
console.log("Adding child");
$.ajax({
url: "/api/product/" + this.pid,
method: "PUT",
data: {
title: this.newtitle
}
}).done(this.created_child);
},
rename() {
$.ajax({
url: "/api/product/" + this.pid,
method: "POST",
data: {
title: this.edittitle
}
}).done(this.renamed);
},
renamed() {
window.location.reload();
},
created_child(data) {
window.location.reload();
},
delete_product() {
this.dp_clicks++;
console.log("clicked " + this.dp_clicks);
if (this.dp_clicks >= 2) {
$.ajax({
url: "/api/product/" + this.pid,
method: "DELETE",
}).done(this.done_delete);
return;
}
this.dp_class = "btn-warning";
setTimeout(this.undo_delete, 1000);
},
undo_delete() {
console.log("Undo delete");
this.dp_class = "btn-danger";
this.dp_clicks = 0;
},
done_delete(data) {
document.location="/documents/" + data.id;
},
empty_trash() {
$.ajax({
url: "/api/product/trash/" + this.prod.id,
method: "DELETE"
}).done(this.trash_emptied);
},
trash_emptied() {
window.location.reload();
},
gemini_all() {
$.ajax({
url: "/api/product/gemini_all/" + this.prod.id + "/keep",
method: "GET"
}).done(this.gemini_all_queued);
},
gemini_all_move() {
$.ajax({
url: "/api/product/gemini_all/" + this.prod.id + "/move",
method: "GET"
}).done(this.gemini_all_queued);
},
gemini_all_queued(d) {
},
},
mounted() {
this.init();
},
};
</script>

Some files were not shown because too many files have changed in this diff Show More