Initial import
This commit is contained in:
14
.env
Normal file
14
.env
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
DEBUG=true
|
||||||
|
|
||||||
|
DB_HOST=db
|
||||||
|
DB_USER=something
|
||||||
|
DB_PASS=whatever
|
||||||
|
DB_DATABASE=whoever
|
||||||
|
|
||||||
|
|
||||||
|
GEMINI_KEY_0=blahblahblah
|
||||||
|
GEMINI_KEY_1=barbar
|
||||||
|
GEMINI_KEY_2=foofoo
|
||||||
|
|
||||||
|
MAGICK=convert
|
||||||
10
Makefile
Normal file
10
Makefile
Normal 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/
|
||||||
169
app.php
Executable file
169
app.php
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
<?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 ($_SERVER['argc'] == 0) {
|
||||||
|
$blade = new Blade(ROOT . '/views', ROOT . '/cache');
|
||||||
|
$req = new Request;
|
||||||
|
App::dispatch($req);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
app/controllers/CoverController.php
Normal file
66
app/controllers/CoverController.php
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
379
app/controllers/DocumentController.php
Normal file
379
app/controllers/DocumentController.php
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
<?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) {
|
||||||
|
|
||||||
|
$job = new GeminiJob($id, "document:$id");
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/controllers/DownloadController.php
Normal file
31
app/controllers/DownloadController.php
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
app/controllers/HomeController.php
Normal file
7
app/controllers/HomeController.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class HomeController {
|
||||||
|
static function index() {
|
||||||
|
return blade("index");
|
||||||
|
}
|
||||||
|
}
|
||||||
78
app/controllers/ImportController.php
Normal file
78
app/controllers/ImportController.php
Normal 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
app/controllers/JobController.php
Normal file
23
app/controllers/JobController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
app/controllers/PDFController.php
Normal file
55
app/controllers/PDFController.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class PDFController {
|
||||||
|
public static function download($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"];
|
||||||
|
}
|
||||||
|
|
||||||
|
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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
161
app/controllers/ProductController.php
Normal file
161
app/controllers/ProductController.php
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<?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) {
|
||||||
|
$prod = new Product($id);
|
||||||
|
|
||||||
|
$c = new Collection();
|
||||||
|
|
||||||
|
foreach ($prod->documents as $doc) {
|
||||||
|
$job = new GeminiJob($doc->id, "document:" . $doc->id);
|
||||||
|
$jobid = $job->queue();
|
||||||
|
$c->push(["document" => $doc->id, "job" => $jobid]);
|
||||||
|
}
|
||||||
|
return $c;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/controllers/RevisionController.php
Normal file
46
app/controllers/RevisionController.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class RevisionController {
|
||||||
|
|
||||||
|
public static function show($id) {
|
||||||
|
|
||||||
|
$rev = new Revision($id);
|
||||||
|
$rev->load("document");
|
||||||
|
|
||||||
|
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 DownloadJob($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);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/controllers/SearchController.php
Normal file
76
app/controllers/SearchController.php
Normal 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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
92
app/controllers/SpiderController.php
Normal file
92
app/controllers/SpiderController.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?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;
|
||||||
|
|
||||||
|
$job->queued = time();
|
||||||
|
$job->started = 0;
|
||||||
|
$job->finished = 0;
|
||||||
|
$job->processed = 0;
|
||||||
|
$job->url = $pdf->url;
|
||||||
|
$job->owner = get_user()->id;
|
||||||
|
$job->file = sprintf("download/file-%08X-%08X", rand(), time());
|
||||||
|
|
||||||
|
$job->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
79
app/controllers/SystemController.php
Normal file
79
app/controllers/SystemController.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
57
app/jobs/DownloadJob.php
Normal file
57
app/jobs/DownloadJob.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class DownloadJob extends Job {
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
print_r($this);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
app/jobs/GeminiJob.php
Normal file
214
app/jobs/GeminiJob.php
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class GeminiJob extends Job {
|
||||||
|
|
||||||
|
public $docid;
|
||||||
|
|
||||||
|
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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->status("Processing document with Gemini");
|
||||||
|
try {
|
||||||
|
$gemini = new Gemini();
|
||||||
|
$gemini->upload_callback([$this, "uploadcb"]);
|
||||||
|
$gemini->process_callback([$this, "processcb"]);
|
||||||
|
$lines = $gemini->geminiOverview($pdf);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->status($e->getMessage());
|
||||||
|
if ($e->getmessage() == "The request timed out. Please try again.") {
|
||||||
|
$this->retry();
|
||||||
|
} else if ($e->getmessage() == "HTTP Error 0 requesting AI assistance") {
|
||||||
|
$this->retry();
|
||||||
|
} else {
|
||||||
|
$this->fail();
|
||||||
|
}
|
||||||
|
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->internal_id = $iid[0];
|
||||||
|
}
|
||||||
|
array_shift($ld);
|
||||||
|
}
|
||||||
|
$doc->overview = implode("\n", $ld);
|
||||||
|
|
||||||
|
$doc->save();
|
||||||
|
/*
|
||||||
|
$db = DB::getInstance();
|
||||||
|
|
||||||
|
$q = $db->query("
|
||||||
|
select
|
||||||
|
product.id as id,
|
||||||
|
locate(product.title, document.title) as wordoffset
|
||||||
|
from
|
||||||
|
product,
|
||||||
|
document
|
||||||
|
where
|
||||||
|
locate(product.title, document.title) > 0 and
|
||||||
|
document.id = :id and
|
||||||
|
length(product.title) > 3
|
||||||
|
order by
|
||||||
|
length(product.title) desc
|
||||||
|
limit 0,1
|
||||||
|
", ["id" => $doc->id]);
|
||||||
|
|
||||||
|
if ($r = $db->nextRecord($q)) {
|
||||||
|
$d = new DocProduct();
|
||||||
|
$d->document = $doc->id;
|
||||||
|
$d->product = $r->id;
|
||||||
|
$d->save();
|
||||||
|
} else {
|
||||||
|
$q = $db->query("
|
||||||
|
select
|
||||||
|
product.id as id,
|
||||||
|
locate(product.title, document.overview) as wordoffset
|
||||||
|
from
|
||||||
|
product,
|
||||||
|
document
|
||||||
|
where
|
||||||
|
locate(product.title, document.overview) > 0 and
|
||||||
|
locate(product.title, document.overview) < 300 and
|
||||||
|
document.id = :id and
|
||||||
|
length(product.title) > 3
|
||||||
|
order by
|
||||||
|
length(product.title) desc
|
||||||
|
limit 0,1
|
||||||
|
", ["id" => $doc->id]);
|
||||||
|
|
||||||
|
if ($r = $db->nextRecord($q)) {
|
||||||
|
$d = new DocProduct();
|
||||||
|
$d->document = $doc->id;
|
||||||
|
$d->product = $r->id;
|
||||||
|
$d->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
$this->status("Finished");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
app/jobs/ProcessJob.php
Normal file
15
app/jobs/ProcessJob.php
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class ProcessJob extends Job {
|
||||||
|
|
||||||
|
public $docid = 0;
|
||||||
|
|
||||||
|
public function __construct($docid) {
|
||||||
|
parent::__construct("document:$docid");
|
||||||
|
$this->docid = $docid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
app/models/DocMeta.php
Normal file
13
app/models/DocMeta.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class DocMeta extends Model {
|
||||||
|
|
||||||
|
protected $_classes = [
|
||||||
|
"document" => "Document",
|
||||||
|
"metadata" => "MetaType"
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
17
app/models/DocProduct.php
Normal file
17
app/models/DocProduct.php
Normal 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"]
|
||||||
|
];
|
||||||
|
}
|
||||||
191
app/models/Document.php
Normal file
191
app/models/Document.php
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<?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]])->orderBy("revno")->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_related() {
|
||||||
|
$revs = Revision::find([["document", "=", $this->id]])->all();
|
||||||
|
$related = new Collection;
|
||||||
|
foreach ($revs as $rev) {
|
||||||
|
$q = DB::getInstance()->query("
|
||||||
|
select
|
||||||
|
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) {
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
54
app/models/IDMatch.php
Normal file
54
app/models/IDMatch.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?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) {
|
||||||
|
print("<<$text>>\n");
|
||||||
|
$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);
|
||||||
|
|
||||||
|
print(">>$text<<\n");
|
||||||
|
|
||||||
|
//$text = preg_replace("/([A-Za-z])(- )/", '$1', $text);
|
||||||
|
|
||||||
|
$words = preg_split('/\s+/', $text);
|
||||||
|
return $words;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
11
app/models/MetaType.php
Normal file
11
app/models/MetaType.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/models/OCR.php
Normal file
4
app/models/OCR.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class OCR extends Model {
|
||||||
|
}
|
||||||
163
app/models/Product.php
Normal file
163
app/models/Product.php
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
114
app/models/Revision.php
Normal file
114
app/models/Revision.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?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) {
|
||||||
|
$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 "";
|
||||||
|
}
|
||||||
|
$this->_body = $ocr->body;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
app/models/Spider.php
Normal file
4
app/models/Spider.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Spider extends Model {
|
||||||
|
}
|
||||||
4
app/models/SpiderBanned.php
Normal file
4
app/models/SpiderBanned.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SpiderBanned extends Model {
|
||||||
|
}
|
||||||
4
app/models/SpiderDom.php
Normal file
4
app/models/SpiderDom.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SpiderDom extends Model {
|
||||||
|
}
|
||||||
5
app/models/SpiderPage.php
Normal file
5
app/models/SpiderPage.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class SpiderPage extends Model {
|
||||||
|
protected $table="pages";
|
||||||
|
}
|
||||||
5
app/models/User.php
Normal file
5
app/models/User.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class User extends Model {
|
||||||
|
public static $table = "users";
|
||||||
|
}
|
||||||
4583
backend/.done
Executable file
4583
backend/.done
Executable file
File diff suppressed because it is too large
Load Diff
39
backend/JobRunner
Executable file
39
backend/JobRunner
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$job = Job::consumeNextJob($class);
|
||||||
|
if ($job) {
|
||||||
|
print("Executing job " . $job->jobID() . "\n");
|
||||||
|
try {
|
||||||
|
$job->run();
|
||||||
|
$job->finish();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
print_r($e);
|
||||||
|
$job->status($e->getMessage());
|
||||||
|
$job->fail();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
print_r($e);
|
||||||
|
$job->status($e->get_message());
|
||||||
|
$job->fail();
|
||||||
|
}
|
||||||
|
print("Job finished\n");
|
||||||
|
} else {
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
composer.json
Executable file
7
composer.json
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"michelf/php-markdown": "^2.0",
|
||||||
|
"jenssegers/blade": "^2.0",
|
||||||
|
"illuminate/view": "11.7.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lib/AntiSpam.php
Executable file
12
lib/AntiSpam.php
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class AntiSpam {
|
||||||
|
public static function tarpit() {
|
||||||
|
while (true) {
|
||||||
|
print(chr(rand() & 0xff));
|
||||||
|
ob_flush();
|
||||||
|
flush();
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/App.php
Executable file
77
lib/App.php
Executable 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"));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
174
lib/Auth.php
Executable file
174
lib/Auth.php
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
<?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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ($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
lib/Collection.php
Executable file
193
lib/Collection.php
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/Config.php
Executable file
21
lib/Config.php
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
<?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");
|
||||||
|
}
|
||||||
|
if (array_key_exists($val, Config::$config)) {
|
||||||
|
return Config::$config[$val];
|
||||||
|
} else {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
141
lib/DB.php
Executable file
141
lib/DB.php
Executable 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
lib/ErrorHandler.php
Executable file
103
lib/ErrorHandler.php
Executable 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
128
lib/File.php
Executable file
128
lib/File.php
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/Form.php
Executable file
44
lib/Form.php
Executable 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)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
240
lib/Gemini.php
Normal file
240
lib/Gemini.php
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<?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 static function get_key() {
|
||||||
|
$i = 0;
|
||||||
|
$keys = [];
|
||||||
|
|
||||||
|
while ($k = Config::get("GEMINI_KEY_" . $i)) {
|
||||||
|
$keys[] = $k;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyid = rand(0, count($keys) - 1);
|
||||||
|
|
||||||
|
$key = $keys[$keyid];
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function upload_file(File $file, $key = false) {
|
||||||
|
|
||||||
|
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': '" . $file->basename() . "'}}";
|
||||||
|
|
||||||
|
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(File $file) {
|
||||||
|
|
||||||
|
$cb = false;
|
||||||
|
if ($this->_process_callback) $cb = $this->_process_callback;
|
||||||
|
|
||||||
|
if ($cb) $cb("Uploading file");
|
||||||
|
|
||||||
|
if ($this->verbose) {
|
||||||
|
print("<pre>");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = Gemini::get_key();
|
||||||
|
|
||||||
|
$uri = $this->upload_file($file, $key);
|
||||||
|
|
||||||
|
|
||||||
|
$ob = [
|
||||||
|
"contents" => [
|
||||||
|
[
|
||||||
|
"parts" => [
|
||||||
|
[
|
||||||
|
"text" => "Summarize this document. In addition provide me the title of the document as the first line, surrounded by { and }. If it is all in capitals, re-capitalize it to make it easier to read. On the second line give the document order number surrounded by [ and ].",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"file_data" => [
|
||||||
|
"mime_type" => $file->mime(),
|
||||||
|
"file_uri" => $uri
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$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/gemini-2.5-flash-lite: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
lib/HTTPRequest.php
Normal file
112
lib/HTTPRequest.php
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
124
lib/Image.php
Executable file
124
lib/Image.php
Executable 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
119
lib/Job.php
Normal file
119
lib/Job.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class Job {
|
||||||
|
private $_jobid = 0;
|
||||||
|
private $_source = "unknown";
|
||||||
|
|
||||||
|
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() {
|
||||||
|
$class = get_called_class();
|
||||||
|
$data = serialize($this);
|
||||||
|
$db = DB::getInstance();
|
||||||
|
|
||||||
|
$this->_jobid = $db->insert("job", [
|
||||||
|
"class" => $class,
|
||||||
|
"data" => $data,
|
||||||
|
"queued" => time(),
|
||||||
|
"source" => $this->_source,
|
||||||
|
"status" => "Queued",
|
||||||
|
]);
|
||||||
|
|
||||||
|
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" => $txt
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
$db = DB::getInstance();
|
||||||
|
$db->update("job", $this->_jobid, [
|
||||||
|
"failed" => time(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the job.
|
||||||
|
public function retry() {
|
||||||
|
$db = DB::getInstance();
|
||||||
|
$db->update("job", $this->_jobid, [
|
||||||
|
"failed" => 0,
|
||||||
|
"finished" => 0,
|
||||||
|
"started" => 0,
|
||||||
|
"status" => "Retrying"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 queued < unix_timestamp(now()) order by queued limit 1");
|
||||||
|
} else {
|
||||||
|
$q = $db->query("select * from job where started=0 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);
|
||||||
|
return $ob;
|
||||||
|
}
|
||||||
|
}
|
||||||
491
lib/Model.php
Executable file
491
lib/Model.php
Executable file
@@ -0,0 +1,491 @@
|
|||||||
|
<?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) {
|
||||||
|
$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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
145
lib/PDF.php
Executable file
145
lib/PDF.php
Executable file
@@ -0,0 +1,145 @@
|
|||||||
|
<?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($verbose=false) {
|
||||||
|
$g = new Gemini();
|
||||||
|
$g->verbose = $verbose;
|
||||||
|
try {
|
||||||
|
return $g->geminiOverview($this);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
lib/Process.php
Executable file
82
lib/Process.php
Executable 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
112
lib/Query.php
Executable file
112
lib/Query.php
Executable 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
lib/Request.php
Executable file
112
lib/Request.php
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
lib/Route.php
Executable file
98
lib/Route.php
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Route {
|
||||||
|
public $method = "GET";
|
||||||
|
public $pattern = "";
|
||||||
|
public $function = null;
|
||||||
|
public $auth = false;
|
||||||
|
public $args = [];
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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"];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/Routes.php
Executable file
35
lib/Routes.php
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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)) {
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
lib/Session.php
Executable file
71
lib/Session.php
Executable 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
package.json
Normal file
21
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
public/.htaccess
Normal file
10
public/.htaccess
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
# Redirect Trailing Slashes If Not A Folder...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} (.+)/$
|
||||||
|
RewriteRule ^ %1 [L,R=301]
|
||||||
|
|
||||||
|
# Send Requests To Front Controller...
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
2250
public/app.js
Normal file
2250
public/app.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/d192.png
Normal file
BIN
public/d192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/d512.png
Normal file
BIN
public/d512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
3
public/index.php
Normal file
3
public/index.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
require_once(__DIR__ . "/../app.php");
|
||||||
|
|
||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
20
public/manifest.webmanifest
Normal file
20
public/manifest.webmanifest
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
public/sys.css
Normal file
5
public/sys.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.table td.fit,
|
||||||
|
.table th.fit {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1%
|
||||||
|
}
|
||||||
74
routes/api.php
Normal file
74
routes/api.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?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}", ["ProductController", "api_gemini_all"], ["Auth", "can_moderate"]);
|
||||||
73
routes/web.php
Normal file
73
routes/web.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?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}", ["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/attachment/{id}", ["DocumentController", "upload_attachment"], ["Auth", "can_moderate"]);
|
||||||
|
Routes::add_web("POST", "/upload/attachment/{id}", ["DocumentController", "do_upload_attachment"], ["Auth", "can_moderate"]);
|
||||||
|
|
||||||
90
src/DDDiv.vue
Executable file
90
src/DDDiv.vue
Executable 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
src/DocumentEdit.vue
Executable file
195
src/DocumentEdit.vue
Executable 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
src/Download.vue
Executable file
127
src/Download.vue
Executable 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
src/DownloadManager.vue
Executable file
76
src/DownloadManager.vue
Executable 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
src/IDMatch.vue
Executable file
249
src/IDMatch.vue
Executable 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
src/Imports.vue
Executable file
550
src/Imports.vue
Executable 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
src/InlineEdit.vue
Executable file
85
src/InlineEdit.vue
Executable 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
src/InlineEditText.vue
Executable file
114
src/InlineEditText.vue
Executable 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
src/ItemAdder.vue
Normal file
87
src/ItemAdder.vue
Normal 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>
|
||||||
78
src/JobDisplay.vue
Executable file
78
src/JobDisplay.vue
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
<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.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([])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
update() {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/jobs/" + this.source,
|
||||||
|
method: "GET"
|
||||||
|
}).done(this.updated);
|
||||||
|
},
|
||||||
|
|
||||||
|
updated(d) {
|
||||||
|
this.jobs = d;
|
||||||
|
|
||||||
|
var shouldupdate = 0;
|
||||||
|
for (var i = 0; i < this.jobs.length; i++) {
|
||||||
|
console.log(this.jobs[i]);
|
||||||
|
if ((this.jobs[i].finished == 0) && (this.jobs[i].failed == 0)) {
|
||||||
|
shouldupdate = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(shouldupdate);
|
||||||
|
if (shouldupdate == 1) {
|
||||||
|
setTimeout(this.update, 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.updated);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
34
src/KeyboardInteraction.vue
Normal file
34
src/KeyboardInteraction.vue
Normal 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>
|
||||||
67
src/PDF.vue
Executable file
67
src/PDF.vue
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row m-3 bg-white">
|
||||||
|
<div class="col-6 m-0 p-0">
|
||||||
|
<img @click="nav_left" class="w-100 m-0 p-0" :src="left">
|
||||||
|
</div>
|
||||||
|
<div class="col-6 m-0 p-0">
|
||||||
|
<img @click="nav_right" class="w-100 m-0 p-0" :src="right">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue/dist/vue.esm-bundler';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
rev: ref("")
|
||||||
|
},
|
||||||
|
|
||||||
|
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";
|
||||||
|
},
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.left = '';
|
||||||
|
this.right = '';
|
||||||
|
this.leftpage = 0;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
nav_right() {
|
||||||
|
this.leftpage += 2;
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
nav_left() {
|
||||||
|
this.leftpage -= 2;
|
||||||
|
if (this.leftpage < 0) {
|
||||||
|
this.leftpage = 0;
|
||||||
|
}
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
123
src/PDFList.vue
Executable file
123
src/PDFList.vue
Executable 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>
|
||||||
180
src/ProductControls.vue
Executable file
180
src/ProductControls.vue
Executable file
@@ -0,0 +1,180 @@
|
|||||||
|
<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 fa-magic-wand-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,
|
||||||
|
method: "GET"
|
||||||
|
}).done(this.gemini_all_queued);
|
||||||
|
},
|
||||||
|
|
||||||
|
gemini_all_queued(d) {
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
172
src/ProductSelector.vue
Executable file
172
src/ProductSelector.vue
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="p in products" class="btn-group m-1">
|
||||||
|
<button class="btn btn-secondary p-1" :title="p.full_path"><small>{{ p.title }}</small></button>
|
||||||
|
<button :pid="p.id" @click.stop="remove(p.id)" class="btn btn-secondary p-1" title="Remove"><small><i class="fa fa-close"></i></small></button>
|
||||||
|
</div>
|
||||||
|
<div ref="dropdown" class="dropdown" @show.bs.dropdown="add_show">
|
||||||
|
<button class="btn btn-success p-1 dropdown-toggle" title="Add" data-bs-toggle="dropdown" @click="add_show"><small><i class="fa fa-plus"></i></small></button>
|
||||||
|
<ul class="dropdown-menu" @show.bs.dropdown="add_show">
|
||||||
|
<li v-for="p in mru"><a href='#' @click="add_pid(p)" class="dropdown-item">{{ p.full_path }}</a></li>
|
||||||
|
<hr>
|
||||||
|
<li><input ref="searchinput" @input="do_search" class='form-control' name='search' v-model="search" autocomplete="off"></li>
|
||||||
|
<li v-for="p in searchres"><a href='#' @click="add_pid(p)" class="dropdown-item">{{ p.full_path }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button @click="set_default_pids" class="btn btn-primary">Set Default</button>
|
||||||
|
<button @click="clear_default_pids" class="btn btn-primary">Clear Default</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, computed } from 'vue/dist/vue.esm-bundler';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
modelValue: ref(""),
|
||||||
|
},
|
||||||
|
setup(props, {emit}) {
|
||||||
|
return {
|
||||||
|
products: ref([]),
|
||||||
|
mru: ref([]),
|
||||||
|
search: ref(""),
|
||||||
|
searchres: ref([]),
|
||||||
|
pids: computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
}),
|
||||||
|
was_mod: ref(false),
|
||||||
|
req: ref(null),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
$(document).on("show.bs.dropdown", '.dropdown', this.add_show);
|
||||||
|
$(document).on("shown.bs.dropdown", $(this.$refs.dropdown), this.add_shown);
|
||||||
|
$(this.$refs.dropdown).on("show.bs.dropdown", this.add_show);
|
||||||
|
$(this.$refs.dropdown).on("shown.bs.dropdown", this.add_shown);
|
||||||
|
this.get_products();
|
||||||
|
|
||||||
|
this.$watch('modelValue', this.get_products);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
get_products() {
|
||||||
|
if (this.pids!= "") {
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/product/" + this.pids,
|
||||||
|
method: "GET"
|
||||||
|
}).done(this.got_products);
|
||||||
|
} else {
|
||||||
|
this.products = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var m = this.was_mod;
|
||||||
|
this.was_mod = false;
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
if (this.pids == "") {
|
||||||
|
// Get default if available
|
||||||
|
var p = localStorage.getItem("default_pids");
|
||||||
|
if ((p != null) && (p != "")) {
|
||||||
|
this.was_mod = true;
|
||||||
|
this.pids = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
got_products(data) {
|
||||||
|
this.products = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(e) {
|
||||||
|
this.was_mod = true;
|
||||||
|
var tmp = [];
|
||||||
|
var pidarray = [];
|
||||||
|
for (var i = 0; i < this.products.length; i++) {
|
||||||
|
if (this.products[i].id != e) {
|
||||||
|
tmp.push(this.products[i]);
|
||||||
|
pidarray.push(this.products[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.products = tmp;
|
||||||
|
this.pids = pidarray.join(",");
|
||||||
|
},
|
||||||
|
|
||||||
|
add_show(e) {
|
||||||
|
this.search = "";
|
||||||
|
this.searchres = []
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/productmru",
|
||||||
|
method: "GET"
|
||||||
|
}).done(this.fill_mru);
|
||||||
|
},
|
||||||
|
|
||||||
|
add_shown(e) {
|
||||||
|
this.$refs.searchinput.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
fill_mru(data) {
|
||||||
|
this.mru = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
do_search(e) {
|
||||||
|
if (this.search.length >= 3) {
|
||||||
|
if (this.req != null) {
|
||||||
|
this.req.abort();
|
||||||
|
this.req = null;
|
||||||
|
}
|
||||||
|
this.req=$.ajax({
|
||||||
|
url: "/api/search/product",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
search: this.search
|
||||||
|
}
|
||||||
|
}).done(this.search_done);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
search_done(data) {
|
||||||
|
this.searchres = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
add_pid(prod) {
|
||||||
|
this.was_mod = true;
|
||||||
|
var pidarray;
|
||||||
|
if (this.pids == "") {
|
||||||
|
pidarray = [];
|
||||||
|
} else {
|
||||||
|
pidarray = this.pids.split(",");
|
||||||
|
}
|
||||||
|
if (pidarray.includes(prod.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pidarray.push(prod.id);
|
||||||
|
|
||||||
|
this.pids = pidarray.join(",");
|
||||||
|
this.products.push(prod);
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/productmru/" + prod.id,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_default_pids() {
|
||||||
|
localStorage.setItem("default_pids", this.pids);
|
||||||
|
},
|
||||||
|
|
||||||
|
clear_default_pids() {
|
||||||
|
localStorage.setItem("default_pids", "");
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
85
src/Search.vue
Executable file
85
src/Search.vue
Executable file
@@ -0,0 +1,85 @@
|
|||||||
|
<template>
|
||||||
|
<div tabindex=0 @blur.capture="click_close">
|
||||||
|
<form action="/search" method="POST">
|
||||||
|
<div class="input-group">
|
||||||
|
<input name="search" ref="sb" @focus="key_search" @keyup="key_search" class="form-control" v-model="search">
|
||||||
|
<input type="submit" class="btn btn-success" value="Search">
|
||||||
|
</div>
|
||||||
|
<div v-if="results.length > 0" class="z-3 position-absolute bg-light">
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="r in results"><a :href="'/document/' + r.id"><strong>{{ r.title }}</strong> {{ r.subtitle }} <small>{{ r.subsubtitle }}</small></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref } from 'vue/dist/vue.esm-bundler';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
],
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
search: ref(""),
|
||||||
|
searchTimer: ref(null),
|
||||||
|
results: ref([]),
|
||||||
|
req: ref(null),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
click_close(event) {
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
this.results = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
key_search() {
|
||||||
|
if (this.searchTimer != null) {
|
||||||
|
window.clearTimeout(this.searchTimer);
|
||||||
|
this.searchTimer = null;
|
||||||
|
}
|
||||||
|
this.searchTimer = setTimeout(this.do_key_search, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
do_key_search() {
|
||||||
|
if (this.search.length < 3) {
|
||||||
|
this.results = [];
|
||||||
|
} else {
|
||||||
|
this.searchTimer = null;
|
||||||
|
if (this.req != null) {
|
||||||
|
this.req.abort();
|
||||||
|
this.req = null;
|
||||||
|
}
|
||||||
|
this.req = $.ajax({
|
||||||
|
url: "/api/search/title",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
search: this.search
|
||||||
|
}
|
||||||
|
}).done(this.got_results);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
got_results(data) {
|
||||||
|
this.results = data;
|
||||||
|
},
|
||||||
|
shortcut(e) {
|
||||||
|
if (e.target.tagName == "INPUT") return;
|
||||||
|
if (e.target.tagName == "TEXTAREA") return;
|
||||||
|
if (e.key == "/") {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this.$refs.sb).focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
$(document).on("keydown", this.shortcut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
79
src/SpiderPages.vue
Executable file
79
src/SpiderPages.vue
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li v-for="page in pages" class="list-group-item">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-11 col-12 text-center">
|
||||||
|
<a class="btn text-left w-100" :href="page.url" target="_blank">
|
||||||
|
<h6>{{ page.title }}</h6>
|
||||||
|
{{ page.url }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group col-lg-1 col-12">
|
||||||
|
<button :disabled="busy" @click="reject(page)" class="btn btn-danger"><i class="fa fa-close"></i></button>
|
||||||
|
<button :disabled="busy" @click="accept(page)" 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 {
|
||||||
|
pages: ref([]),
|
||||||
|
busy: ref(false),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.busy = true;
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/spider/pages",
|
||||||
|
method: "GET"
|
||||||
|
}).done(this.got_update);
|
||||||
|
},
|
||||||
|
|
||||||
|
got_update(data) {
|
||||||
|
this.pages = data;
|
||||||
|
this.busy = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
accept(page) {
|
||||||
|
if (this.busy) return;
|
||||||
|
this.busy = true;
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/spider/page/" + page.id,
|
||||||
|
method: "PUT"
|
||||||
|
}).done(this.got_update)
|
||||||
|
},
|
||||||
|
|
||||||
|
reject(page) {
|
||||||
|
if (this.busy) return;
|
||||||
|
this.busy = true;
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/spider/page/" + page.id,
|
||||||
|
method: "DELETE"
|
||||||
|
}).done(this.got_update)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
24
src/Template.vue
Executable file
24
src/Template.vue
Executable 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>
|
||||||
9
src/app.css
Executable file
9
src/app.css
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
.table td.fit,
|
||||||
|
.table th.fit {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1%
|
||||||
|
}
|
||||||
|
|
||||||
|
a[title]:hover::after {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
52
src/app.js
Executable file
52
src/app.js
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
import 'jquery';
|
||||||
|
import 'jquery-simple-websocket';
|
||||||
|
import 'bootstrap';
|
||||||
|
import '@fortawesome/fontawesome-free/js/fontawesome';
|
||||||
|
import '@fortawesome/fontawesome-free/js/solid';
|
||||||
|
import '@fortawesome/fontawesome-free/js/regular';
|
||||||
|
import '@fortawesome/fontawesome-free/js/brands';
|
||||||
|
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
|
import { createApp } from 'vue/dist/vue.esm-bundler';
|
||||||
|
|
||||||
|
import Download from './Download.vue';
|
||||||
|
import DocumentEdit from './DocumentEdit.vue';
|
||||||
|
import ProductSelector from './ProductSelector.vue';
|
||||||
|
import ProductControls from './ProductControls.vue';
|
||||||
|
import InlineEdit from './InlineEdit.vue';
|
||||||
|
import InlineEditText from './InlineEditText.vue';
|
||||||
|
import Search from './Search.vue';
|
||||||
|
import PDFList from './PDFList.vue';
|
||||||
|
import SpiderPages from './SpiderPages.vue';
|
||||||
|
import DownloadManager from './DownloadManager.vue';
|
||||||
|
import Imports from './Imports.vue';
|
||||||
|
import IDMatch from './IDMatch.vue';
|
||||||
|
import DDDiv from './DDDiv.vue';
|
||||||
|
import PDF from './PDF.vue';
|
||||||
|
import ItemAdder from './ItemAdder.vue';
|
||||||
|
import KeyboardInteraction from './KeyboardInteraction.vue';
|
||||||
|
import JobDisplay from './JobDisplay.vue';
|
||||||
|
|
||||||
|
const app = createApp({ });
|
||||||
|
|
||||||
|
app.component("download", Download);
|
||||||
|
app.component("documentedit", DocumentEdit);
|
||||||
|
app.component("productselector", ProductSelector);
|
||||||
|
app.component("productcontrols", ProductControls);
|
||||||
|
app.component("inlineedit", InlineEdit);
|
||||||
|
app.component("inlineedittext", InlineEditText);
|
||||||
|
app.component("search", Search);
|
||||||
|
app.component("pdflist", PDFList);
|
||||||
|
app.component("spiderpages", SpiderPages);
|
||||||
|
app.component("downloadmanager", DownloadManager);
|
||||||
|
app.component("imports", Imports);
|
||||||
|
app.component("idmatch", IDMatch);
|
||||||
|
app.component("dddiv", DDDiv);
|
||||||
|
app.component("pdf", PDF);
|
||||||
|
app.component("itemadder", ItemAdder);
|
||||||
|
app.component("keyboardinteraction", KeyboardInteraction);
|
||||||
|
app.component("jobdisplay", JobDisplay);
|
||||||
|
|
||||||
|
$(window).on("load", function() { app.mount('#app') });
|
||||||
0
src/app.scss
Executable file
0
src/app.scss
Executable file
8
views/404.blade.php
Normal file
8
views/404.blade.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
<img src='/logo.png'>
|
||||||
|
<p>The page you were looking for could not be found. Maybe you should look for a different one instead.</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
94
views/account.blade.php
Normal file
94
views/account.blade.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs mt-3 mb-2">
|
||||||
|
<li class="nav-item">
|
||||||
|
@if ($tab == "account")
|
||||||
|
<a class="nav-link active" aria-current="page" href="#">Account</a>
|
||||||
|
@else
|
||||||
|
<a class="nav-link" aria-current="page" href="/account">Account</a>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
@if ($tab == "data")
|
||||||
|
<a class="nav-link active" aria-current="page" href="#">Data</a>
|
||||||
|
@else
|
||||||
|
<a class="nav-link" aria-current="page" href="/account/data">Data</a>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if ($tab == "account")
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-3"></div>
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<h5>Change Password</h5>
|
||||||
|
<form action='/account/chpass' method="POST">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text w-25">Current password</span>
|
||||||
|
<input class="form-control" name="current" type="password">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text w-25">New password</span>
|
||||||
|
<input class="form-control" name="pass1" type="password">
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text w-25">Confirm password</span>
|
||||||
|
<input class="form-control w-50" name="pass2" type="password">
|
||||||
|
<input class="form-control w-25 btn btn-primary" type="submit" value="Change">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-lg-3"></div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@if ($tab == "data")
|
||||||
|
|
||||||
|
<p>Here is a list of all the data associated with your current session.</p>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Data</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach ($session as $k=>$v)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $k }}</td>
|
||||||
|
<td>{{ json_encode($v) }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>And here is the contents of your user's database record.</p>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Data</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach ($user->_data as $k=>$v)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $k }}</td>
|
||||||
|
<td>{{ json_encode($v) }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>And that's it. That's the sum total of the data we currently store.</p>
|
||||||
|
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@endsection
|
||||||
137
views/document.blade.php
Normal file
137
views/document.blade.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<jobdisplay source="document:{{ $doc->id }}"></jobdisplay>
|
||||||
|
@endif
|
||||||
|
<h1 class="text-center">{{ $doc->title }}</h1>
|
||||||
|
<h2 class="text-center">{{ $doc->subtitle }}</h2>
|
||||||
|
<h3 class="text-center">{{ $doc->subsubtitle }}</h3>
|
||||||
|
|
||||||
|
<div class="row" id="droppable">
|
||||||
|
<div class="col-lg-6 col-12">
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<div class="btn-group">
|
||||||
|
<documentedit docid="{{ $doc->id }}"></documentedit>
|
||||||
|
<a href="/upload/attachment/{{ $doc->id }}" class="btn btn-primary" title="Upload attachment"><i class="fa-solid fa-file-arrow-up"></i></a>
|
||||||
|
<a href="/overview/create/{{ $doc->id }}" class="btn btn-primary" title="Create Overview with AI"><i class="fa-solid fa-wand-magic-sparkles"></i></a>
|
||||||
|
<a href="/merge/{{ $doc->id }}" class="btn btn-primary" title="Merge Revisions"><i class="fa fa-link"></i></a>
|
||||||
|
<a href="/explode/{{ $doc->id }}" class="btn btn-primary" title="Separate Revisions"><i class="fa fa-unlink"></i></a>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<h5 class="text-center">Order Number: {{ $doc->internal_id }}</h5>
|
||||||
|
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
@foreach ($doc->metadata as $meta)
|
||||||
|
@php
|
||||||
|
$meta->load("metadata");
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="w-25">{{ $meta->metadata->name }}</td>
|
||||||
|
<td class="w-75"><inlineedit value="{{ $meta->data }}" url="/api/document/{{ $doc->id }}/metadata/{{ $meta->metadata->id }}" field="data"/></td>
|
||||||
|
<td style="width: 0"><a class="btn btn-danger px-1 py-0" href="/document/{{ $doc->id }}/metadata/{{ $meta->metadata->id }}/delete">
|
||||||
|
<i class="fa fa-trash"></i></a></td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<itemadder list="/api/document/{{ $doc->id }}/available_metadata" target="/api/document/{{ $doc->id }}/metadata" method="PUT"></itemadder>
|
||||||
|
@else
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
@foreach ($doc->metadata as $meta)
|
||||||
|
@php
|
||||||
|
$meta->load("metadata");
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="w-25">{{ $meta->metadata->name }}</td>
|
||||||
|
<td class="w-75">{{ $meta->data }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
@if ($doc->attachments->count() > 0)
|
||||||
|
<h5>Attachments</h5>
|
||||||
|
<table class="table w-100">
|
||||||
|
<thead>
|
||||||
|
<th>Filename</th>
|
||||||
|
<th>Size</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($doc->attachments as $f)
|
||||||
|
<tr>
|
||||||
|
<td><a href='/attachment/{{$doc->id}}/{{$f->basename()}}'>{{ $f->basename() }}</a></td>
|
||||||
|
<td>{{ $f->size() }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<ul class="list-group">
|
||||||
|
@foreach ($doc->products as $prod)
|
||||||
|
<li class="list-group-item">
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<a class="btn btn-danger px-1 py-0" href='/del_docproduct/{{$doc->id}}/{{$prod->id}}'>
|
||||||
|
<i class="fa fa-close"></i>
|
||||||
|
</a>
|
||||||
|
<a class="btn w-90 text-start" href='/documents/{{ $prod->id }}'>
|
||||||
|
{{ $prod->full_path }}
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<a class="btn w-100 text-start" href='/documents/{{ $prod->id }}'>
|
||||||
|
{{ $prod->full_path }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
@if ($doc->overview)
|
||||||
|
<div class="overflow-y-auto p-3">
|
||||||
|
{!! $doc->overview_md() !!}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6 col-12">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
@foreach ($doc->revisions as $rev)
|
||||||
|
@component("thumbnail-50", ["rev" => $rev])
|
||||||
|
@endcomponent
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@if (count($doc->related) > 0)
|
||||||
|
<div class="col-12">
|
||||||
|
<h5>Related Documents</h5>
|
||||||
|
<table class="table">
|
||||||
|
@foreach ($doc->related as $r)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href='/document/{{ $r->id }}'>
|
||||||
|
<strong>{{ $r->title }}</strong>
|
||||||
|
{{ $r->subtitle }}
|
||||||
|
<small>{{ $r->subsubtitle }}</small>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href='/document/{{ $r->id }}'>
|
||||||
|
{{ $r->internal_id }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
140
views/documents.blade.php
Executable file
140
views/documents.blade.php
Executable file
@@ -0,0 +1,140 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
<div class="my-4 btn-group">
|
||||||
|
@foreach ($product->get_tree() as $p)
|
||||||
|
<a href='/documents/{{ $p->id }}' class="btn btn-secondary"><strong>{{ $p->title }}</strong></a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<a href='<?php print($product->refurl) ?>'><?php print($product->refurl) ?></a><br/>
|
||||||
|
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<inlineedittext url='/api/product/{{$product->id}}' field='overview'>{{ $product->overview }}</inlineedittext>
|
||||||
|
@else
|
||||||
|
@if ($product->overview != "")
|
||||||
|
<div class="blockquote">
|
||||||
|
{!! $product->overview_md() !!}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<tr>
|
||||||
|
<td colspan="{{ 3 + count($product->meta()) }}">
|
||||||
|
<div class="w-100 text-end">
|
||||||
|
<productcontrols pid="{{ $product->id }}"></productcontrols>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Title</th>
|
||||||
|
@foreach ($product->meta() as $m)
|
||||||
|
<th class="fit">{{ MetaType::name($m) }}</th>
|
||||||
|
@endforeach
|
||||||
|
<th>Order No</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
@if ($product->parent != null)
|
||||||
|
<tr>
|
||||||
|
<td class="fit"><a class="btn" href='/documents/{{ $product->parent->id }}'><i class="fa fa-level-up" aria-hidden="true"></a></td>
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<td class="w-100" colspan="{{ 2 + count($product->meta()) }}">
|
||||||
|
<dddiv droppable="true" object_id="{{$product->parent->id}}" object_type="product" callback="/api/document/dragdrop">
|
||||||
|
<a class="btn w-100 text-start" href='/documents/{{ $product->parent->id }}'>Parent</a>
|
||||||
|
</dddiv>
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
<td class="w-100" colspan="{{ 2 + count($product->meta()) }}"><a class="btn w-100 text-start" href='/documents/{{ $product->parent->id }}'>Parent</a></td>
|
||||||
|
@endif
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
|
@foreach ($product->children as $child)
|
||||||
|
<tr>
|
||||||
|
<td class="fit"><a class="btn" href='/documents/{{ $child->id }}'>
|
||||||
|
@if ($child->title == "Trash")
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
@else
|
||||||
|
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||||
|
@endif
|
||||||
|
</a></td>
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<td class="w-100">
|
||||||
|
<dddiv draggable="true" droppable="true" object_id="{{ $child->id }}" object_type="product" callback="/api/document/dragdrop">
|
||||||
|
<a class="btn w-100 text-start" href='/documents/{{$child->id}}' >
|
||||||
|
{{$child->title}}
|
||||||
|
</a>
|
||||||
|
</dddiv>
|
||||||
|
</td>
|
||||||
|
@else
|
||||||
|
<td class="w-100"><a class="btn w-100 text-start" href='/documents/{{$child->id}}'>{{$child->title}}</a></td>
|
||||||
|
@endif
|
||||||
|
<td colspan="{{ 1 + count($product->meta()) }}" class="fit"></td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
@foreach ($product->documents_sorted_by_meta() as $doc)
|
||||||
|
<tr>
|
||||||
|
<td class="fit">
|
||||||
|
@if ($doc->attachments->count() > 0)
|
||||||
|
<a class="btn" href='/document/{{$doc->id}}'><i class="fa fa-file-arrow-down text-success" aria-hidden="true"></i></a>
|
||||||
|
@else
|
||||||
|
<a class="btn" href='/document/{{$doc->id}}'><i class="fa fa-file-text" aria-hidden="true"></i></a>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="w-100">
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<dddiv draggable="true" droppable="true" object_id="{{$doc->id}}" object_type="document" extra_data="{{$product->id}}" callback="/api/document/dragdrop">
|
||||||
|
<a class="btn w-100 text-start" href='/document/{{$doc->id}}'>
|
||||||
|
<strong>{{$doc->title}}</strong>
|
||||||
|
{{$doc->subtitle}}
|
||||||
|
<small>{{$doc->subsubtitle}}</small>
|
||||||
|
@if ($doc->overview)
|
||||||
|
<small><i title="{{ $doc->overview }}" class="ms-1 fa-solid fa-paragraph"></i></small>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
</dddiv>
|
||||||
|
@else
|
||||||
|
<a class="btn w-100 text-start" href='/document/{{$doc->id}}'>
|
||||||
|
<strong>{{$doc->title}}</strong>
|
||||||
|
{{$doc->subtitle}}
|
||||||
|
<small>{{$doc->subsubtitle}}</small>
|
||||||
|
@if ($doc->overview)
|
||||||
|
<small><i title="{{ $doc->overview }}" class="ms-1 fa-solid fa-paragraph"></i></small>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
@foreach ($product->meta() as $m)
|
||||||
|
<td class="fit">
|
||||||
|
<a draggable="true" object_id="{{$doc->id}}" pid_from="{{$product->id}}" class="btn" href='/document/{{$doc->id}}'>
|
||||||
|
{{ $doc->get_metadata_by_id($m) }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
@endforeach
|
||||||
|
<td class="fit">
|
||||||
|
<a draggable="true" object_id="{{$doc->id}}" pid_from="{{$product->id}}" class="btn" href='/document/{{$doc->id}}'>
|
||||||
|
{{ $doc->internal_id }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<small>
|
||||||
|
<i>
|
||||||
|
{{ count($product->documents) }} documents
|
||||||
|
</i>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
4
views/downloads.blade.php
Normal file
4
views/downloads.blade.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
<downloadmanager></downloadmanager>
|
||||||
|
@endsection
|
||||||
4
views/forms/input.blade.php
Normal file
4
views/forms/input.blade.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{$name}}" class="form-label">{{$title}}</label>
|
||||||
|
<input class="form-control" id="{{$name}}" type="text" name="{{$name}}" value="{{$value}}" {{$attrs}}>
|
||||||
|
</div>
|
||||||
4
views/forms/password.blade.php
Normal file
4
views/forms/password.blade.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{$name}}" class="form-label">{{$title}}</label>
|
||||||
|
<input class="form-control" id="{{$name}}" type="password" name="{{$name}}" value="{{$value}}" {{$attrs}}>
|
||||||
|
</div>
|
||||||
5
views/forms/submit.blade.php
Normal file
5
views/forms/submit.blade.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<div class="mb-3 text-end">
|
||||||
|
<input class="btn btn-primary" type="submit" value="{{$title}}" {{$attrs}}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
4
views/idmatch.blade.php
Normal file
4
views/idmatch.blade.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
<idmatch></idmatch>
|
||||||
|
@endsection
|
||||||
4
views/imports.blade.php
Normal file
4
views/imports.blade.php
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@extends("layout.main")
|
||||||
|
@section("content")
|
||||||
|
<imports></imports>
|
||||||
|
@endsection
|
||||||
46
views/index.blade.php
Normal file
46
views/index.blade.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@extends('layout.main')
|
||||||
|
@section('content')
|
||||||
|
<p class="mt-3">Welcome to <i><b>DECPDF</b></i>, the searchable and organised repository of
|
||||||
|
Digital Equipment Corporation (DEC) documentation, gathering together documents
|
||||||
|
and manuals from around the internet and from users' personal archives.</p>
|
||||||
|
|
||||||
|
<p>We have strived to arrange the content of this site in a logical order
|
||||||
|
so that it finally becomes easy to locate the documentation for your device
|
||||||
|
or system that you need - be it a PDP-11 computer, or an obscure feature
|
||||||
|
of VMS. It should all be there at your fingertips.</p>
|
||||||
|
|
||||||
|
<p>Do you have a document that we don't? Are we missing some vital information
|
||||||
|
that you could provide us with? Then register for an account and request
|
||||||
|
upload permissions, and you can help us to build the ultimate repository
|
||||||
|
of DEC documentation.</p>
|
||||||
|
|
||||||
|
<h3>FAQ</h3>
|
||||||
|
|
||||||
|
<h5>Why do some documents have "Original" and "OCR" versions?</h5>
|
||||||
|
<p>When a new document is uploaded it is tested to see if it has had OCR run on it
|
||||||
|
already. If it has, we use that OCR data for our indexing. If it hasn't, we automatically
|
||||||
|
run OCR on the document. This creates a new version of the document, the OCR version.
|
||||||
|
We keep the original alongside the new OCR version and give you the choice of which
|
||||||
|
version to download.</p>
|
||||||
|
|
||||||
|
<h5>I have a different copy of a document you already have. Can I upload it?</h5>
|
||||||
|
<p>Absolutely. Document revisions with the same main document ID code will be grouped together
|
||||||
|
within the document on the website. We actively seek out other versions of documents, or
|
||||||
|
even other scans of existing revisions of documents, to try and make the archive as complete
|
||||||
|
as possible.</p>
|
||||||
|
|
||||||
|
<h5>What's the Order Number and Revision when I upload?</h5>
|
||||||
|
<p>The order number is DEC's internal document reference ID. It is typically of the format
|
||||||
|
XX-YYYYY-ZZ-RRR, for example EK-3K370-TR-001. The first three sections are the document
|
||||||
|
ID and the last section is the document revision. When you upload please split the document
|
||||||
|
ID from the revision and enter them into the two separate boxes. That way the different
|
||||||
|
revisions of the same document are properly grouped together. Note that not all documents
|
||||||
|
(especially field maintenance print sets) use this format of document IDs, and not all documents
|
||||||
|
even have an ID. In this case, just enter what you can in the document ID and leave the
|
||||||
|
revision either empty or set to 000 (which indicates it's the original unrevised version).
|
||||||
|
If there is no document ID then leave the Order Number blank. A new document ID will be
|
||||||
|
generated as a placeholder for internal use.</p>
|
||||||
|
|
||||||
|
<h5>Will you ever host binary files like disk images and software?</h5>
|
||||||
|
<p>No. That opens up a whole other kettle of worms, alongside a can of fish as well.</p>
|
||||||
|
@endsection
|
||||||
120
views/layout/main.blade.php
Normal file
120
views/layout/main.blade.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<title>Digital PDFs</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
|
|
||||||
|
<script src="/app.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="container">
|
||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary w-100">
|
||||||
|
<div class="container">
|
||||||
|
<div class="d-flex flex-row justify-content-between w-100">
|
||||||
|
<a class="d-none d-lg-inline navbar-brand" href='/'>Digital PDFs</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-1g-0">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href='/documents'>Documents</a>
|
||||||
|
</li>
|
||||||
|
@if (Auth::can_moderate())
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Spider
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/spider_pdfs">Spidered PDFs</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/spider_pages">Potential Sites</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
System
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/status">System Status</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/idmatch">ID Matches</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
@if (Auth::logged_in())
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ get_user()->username }}
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/account">My Account</a></li>
|
||||||
|
|
||||||
|
<?php if (Auth::can_upload()) { ?>
|
||||||
|
<li><a class="dropdown-item" href="/upload">Upload PDF</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/downloads">My Downloads</a></li>
|
||||||
|
<li><a class="dropdown-item" href="/imports">My Imports</a></li>
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
<li><hr></li>
|
||||||
|
<li><a class="dropdown-item text-danger" href="/logout">Log Out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@else
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Guest
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="/register">Register</a></li>
|
||||||
|
<li><hr></li>
|
||||||
|
<li><a class="dropdown-item" href="/login">Log In</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
@endif
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<search/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
@if($flash_error)
|
||||||
|
<div class="my-2 alert alert-danger">{{ $flash_error }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($flash_warn)
|
||||||
|
<div class="my-2 alert alert-warning">{{ $flash_warn }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($flash_info)
|
||||||
|
<div class="my-2 alert alert-info">{{ $flash_info }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($flash_success)
|
||||||
|
<div class="my-2 alert alert-success">{{ $flash_success }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@yield('content')
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<nav class="navbar">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item"><a href='/' class="nav-link small p-0">Home</a></li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item"><a href='/privacy' class="nav-link small p-0">Privacy and Data</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<p class="text-center small text-secondary">Site structure and layout ©2025 Majenko Technologies</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user