summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-09-23 11:01:17 +0200
committerYorhel <git@yorhel.nl>2019-09-23 11:01:17 +0200
commit9ccafba60b8de37b0c2a202d22df5c3e25b78026 (patch)
treeae150b0c25282a646e1d71868022ad4a5789e60a
parent4542d952f782ee193dbf279cd8186cba0e9d87a4 (diff)
v2rw: Convert doc pages + add framework for item fetching & display & revisions
This bumps the minimum Perl version to 5.26 in order to make use of lexical subroutines - a feature I've been wanting for a while. This should be the last version bump, 5.26 is the highest version in Ubuntu LTS at the moment. Not that I use Ubuntu, but it's used by the Docker container and it's a sensible reference. I merged the 'maintabs' and 'hiddenmsg' features into the primary framework_ function; It fits quite well there, removes a little bit of boilerplate from the DB entry page code and reduces the reliance on common "dbSomethingGet()" methods. I was hoping I'd be able to reduce the boilerplate required for defining revisions, but I don't think that's going to happen. What I did do was reimplement the diffing to handle item and text diffs separately, with sensible defaults for the old split/join/diff options. Diffing is now performed on the raw structured data rather than on formatted HTML, which, combined with the db_entry() functions, ought to be less brittle.
-rw-r--r--README.md2
-rw-r--r--lib/VNDB/Handler/Docs.pm106
-rw-r--r--lib/VNDB/Schema.pm2
-rw-r--r--lib/VNWeb/Auth.pm1
-rw-r--r--lib/VNWeb/DB.pm84
-rw-r--r--lib/VNWeb/Docs/Lib.pm90
-rw-r--r--lib/VNWeb/Docs/Page.pm67
-rw-r--r--lib/VNWeb/HTML.pm330
-rw-r--r--lib/VNWeb/Prelude.pm76
-rw-r--r--lib/VNWeb/Validation.pm26
10 files changed, 669 insertions, 115 deletions
diff --git a/README.md b/README.md
index 4cc40624..38bf4292 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Global requirements:
- Linux, or an OS that resembles Linux. Chances are VNDB won't run on Windows.
- PostgreSQL 10 (older versions may work)
-- Perl 5.24+
+- Perl 5.26+
- Elm 0.19
**Perl modules** (core modules are not listed):
diff --git a/lib/VNDB/Handler/Docs.pm b/lib/VNDB/Handler/Docs.pm
index d928a07c..4fabf1d3 100644
--- a/lib/VNDB/Handler/Docs.pm
+++ b/lib/VNDB/Handler/Docs.pm
@@ -7,116 +7,14 @@ use warnings;
use TUWF ':html';
use VNDB::Func;
use Text::MultiMarkdown 'markdown';
+use VNWeb::Docs::Lib;
TUWF::register(
- qr{d([1-9]\d*)(?:\.([1-9]\d*))?} => \&page,
qr{d([1-9]\d*)(?:\.([1-9]\d*))?/edit} => \&edit,
);
-sub _html {
- my $content = shift;
-
- $content =~ s{^:MODERATORS:$}{
- my $l = tuwf->dbUserGet(results => 100, sort => 'id', notperm => tuwf->{default_perm}, what => 'extended');
- my $admin = 0;
- $admin |= $_ for values %{ tuwf->{permissions} };
- '<dl>'.join('', map {
- my $u = $_;
- my $p = $u->{perm} >= $admin ? 'admin' : join ', ', sort map +($u->{perm} &~ tuwf->{default_perm}) & tuwf->{permissions}{$_} ? $_ : (), keys %{ tuwf->{permissions} };
- $p ? sprintf('<dt><a href="/u%d">%s</a></dt><dd>%s</dd>', $_->{id}, $_->{username}, $p) : ()
- } @$l).'</dl>';
- }me;
- $content =~ s{^:SKINCONTRIB:$}{
- my %users;
- push @{$users{ tuwf->{skins}{$_}[1] }}, [ $_, tuwf->{skins}{$_}[0] ]
- for sort { tuwf->{skins}{$a}[0] cmp tuwf->{skins}{$b}[0] } keys %{ tuwf->{skins} };
- my $u = tuwf->dbUserGet(uid => [ keys %users ]);
- '<dl>'.join('', map sprintf('<dt><a href="/u%d">%s</a></dt><dd>%s</dd>',
- $_->{id}, $_->{username}, join(', ', map sprintf('<a href="?skin=%s">%s</a>', $_->[0], $_->[1]), @{$users{$_->{id}}})
- ), @$u).'</dl>';
- }me;
-
- my $html = markdown $content, {
- strip_metadata => 1,
- img_ids => 0,
- disable_footnotes => 1,
- disable_bibliography => 1,
- };
-
- # Number sections and turn them into links
- my($sec, $subsec) = (0,0);
- $html =~ s{<h([1-2])[^>]+>(.*?)</h\1>}{
- if($1 == 1) {
- $sec++;
- $subsec = 0;
- qq{<h3><a href="#$sec" name="$sec">$sec. $2</a></h3>}
- } elsif($1 == 2) {
- $subsec++;
- qq|<h4><a href="#$sec.$subsec" name="$sec.$subsec">$sec.$subsec. $2</a></h4>\n|
- }
- }ge;
-
- # Text::MultiMarkdown doesn't handle fenced code blocks properly. The
- # following solution breaks inline code blocks, but I don't use those anyway.
- $html =~ s/<code>/<pre>/g;
- $html =~ s#</code>#</pre>#g;
-
- $html
-}
-
-
-sub page {
- my($self, $id, $rev) = @_;
-
- my $method = $rev ? 'dbDocGetRev' : 'dbDocGet';
- my $d = $self->$method(id => $id, $rev ? ( rev => $rev ) : ())->[0];
- return $self->resNotFound if !$d->{id};
-
- $self->htmlHeader(title => $d->{title}, noindex => $rev);
- $self->htmlMainTabs(d => $d);
- return if $self->htmlHiddenMessage('d', $d);
-
- if($rev) {
- my $prev = $rev && $rev > 1 && $self->dbDocGetRev(id => $id, rev => $rev-1)->[0];
- $self->htmlRevision('d', $prev, $d,
- [ title => 'Title', diff => 1 ],
- [ content => 'Content', diff => qr/\s+/, short_diff => 1 ],
- );
- }
-
- div class => 'mainbox';
- h1 $d->{title};
- div class => 'docs';
- ul class => 'index';
- li; b 'Guidelines'; end;
- li; a href => '/d5', 'Editing Guidelines'; end;
- li; a href => '/d2', 'Visual Novels'; end;
- li; a href => '/d15', 'Special Games'; end;
- li; a href => '/d3', 'Releases'; end;
- li; a href => '/d4', 'Producers'; end;
- li; a href => '/d16', 'Staff'; end;
- li; a href => '/d12', 'Characters'; end;
- li; a href => '/d10', 'Tags & Traits'; end;
- li; a href => '/d13', 'Capturing Screenshots'; end;
- li; b 'About VNDB'; end;
- li; a href => '/d9', 'Discussion Board'; end;
- li; a href => '/d6', 'FAQ'; end;
- li; a href => '/d7', 'About Us'; end;
- li; a href => '/d17', 'Privacy Policy & Licensing'; end;
- li; a href => '/d11', 'Database API'; end;
- li; a href => '/d14', 'Database Dumps'; end;
- li; a href => '/d18', 'Database Querying'; end;
- li; a href => '/d8', 'Development'; end;
- end;
- lit _html $d->{content};
- end;
- end;
- $self->htmlFooter;
-}
-
-
sub edit {
my($self, $id, $rev) = @_;
@@ -161,7 +59,7 @@ sub edit {
div class => 'mainbox';
h1 'Preview';
div class => 'docs';
- lit _html $frm->{content};
+ lit md2html $frm->{content};
end;
end;
}
diff --git a/lib/VNDB/Schema.pm b/lib/VNDB/Schema.pm
index 1094356c..b6e476b6 100644
--- a/lib/VNDB/Schema.pm
+++ b/lib/VNDB/Schema.pm
@@ -15,6 +15,7 @@ my $ROOT = $INC{'VNDB/Schema.pm'} =~ s{/lib/VNDB/Schema\.pm}{}r;
# Reads schema.sql and returns a hashref with the following structure:
# {
# vn => {
+# name => 'vn',
# dbentry_type => 'v',
# cols => [
# {
@@ -40,6 +41,7 @@ sub schema {
if(/^\s*CREATE\s+TABLE\s+([^ ]+)/) {
die "Unexpected 'CREATE TABLE $1'\n" if $table;
$table = $1;
+ $schema{$table}{name} = $table;
$schema{$table}{dbentry_type} = $1 if /--.*\s+dbentry_type=(.)/;
$schema{$table}{cols} = [];
diff --git a/lib/VNWeb/Auth.pm b/lib/VNWeb/Auth.pm
index 22a96de5..493db3f4 100644
--- a/lib/VNWeb/Auth.pm
+++ b/lib/VNWeb/Auth.pm
@@ -209,7 +209,6 @@ sub resetpass {
my $id = tuwf->dbVali(
select => sql_func(user_resetpass => \$mail, sql_fromhex sha1_hex lc $token)
);
- warn $id;
return $id ? ($id, $token) : ();
}
diff --git a/lib/VNWeb/DB.pm b/lib/VNWeb/DB.pm
index 58a972a8..b839baf3 100644
--- a/lib/VNWeb/DB.pm
+++ b/lib/VNWeb/DB.pm
@@ -6,11 +6,13 @@ use TUWF;
use SQL::Interp ':all';
use Carp 'carp';
use Exporter 'import';
+use VNDB::Schema;
our @EXPORT = qw/
sql
sql_join sql_comma sql_and sql_array sql_func sql_fromhex sql_tohex sql_fromtime sql_totime
enrich enrich_merge enrich_flatten
+ db_entry
/;
@@ -177,4 +179,86 @@ sub enrich_flatten {
}
+
+# Database entry API: Intended to provide a low-level read/write interface for
+# versioned database entires. The same data structure is used for reading and
+# updating entries, and should support easy diffing/comparison.
+# Not very convenient for general querying & searching, those still need custom
+# queries.
+
+
+# Hash table, something like:
+# {
+# v => {
+# prefix => 'vn',
+# base => { .. 'vn_hist' schema }
+# tables => {
+# anime => { .. 'vn_anime_hist' schema }
+# },
+# }, ..
+# }
+my $entry_types = do {
+ my $schema = VNDB::Schema::schema;
+ my %types = map +($_->{dbentry_type}, { prefix => $_->{name} }), grep $_->{dbentry_type}, values %$schema;
+ for my $t (values %$schema) {
+ my $n = $t->{name};
+ my($type) = grep $n =~ s/^$_->{prefix}_//, values %types;
+ next if !$type;
+ $type->{base} = $t if $n eq 'hist';
+ next if $n !~ s/_hist$//;
+ $type->{tables}{$n} = $t;
+ }
+ \%types;
+};
+
+
+# Returns everything for a specific entry ID. The top-level hash also includes
+# the following keys:
+#
+# id, chid, rev, maxrev, hidden, locked, entry_hidden, entry_locked
+#
+# (Ordering of arrays is unspecified)
+#
+# TODO:
+# - Use non _hist tables if $maxrev == $rev (should be faster)
+# - Combine the enrich_merge() calls into a single query.
+sub db_entry {
+ my($type, $id, $rev) = @_;
+ my $t = $entry_types->{$type}||die;
+
+ my $maxrev = tuwf->dbVali('SELECT MAX(rev) FROM changes WHERE type =', \$type, ' AND itemid =', \$id);
+ return undef if !$maxrev;
+ $rev ||= $maxrev;
+ my $entry = tuwf->dbRowi(q{
+ SELECT itemid AS id, id AS chid, rev AS chrev, ihid AS hidden, ilock AS locked
+ FROM changes
+ WHERE}, { type => $type, itemid => $id, rev => $rev }
+ );
+ return undef if !$entry->{id};
+ $entry->{maxrev} = $maxrev;
+
+ if($maxrev == $rev) {
+ $entry->{entry_hidden} = $entry->{hidden};
+ $entry->{entry_locked} = $entry->{locked};
+ } else {
+ my $base = $t->{base}{name} =~ s/_hist$//r;
+ enrich_merge id => "SELECT id, hidden AS entry_hidden, locked AS entry_locked FROM \"$base\" WHERE id IN", $entry;
+ }
+
+ enrich_merge chid => sql(
+ SELECT => sql_comma(map "\"$_->{name}\"", $t->{base}{cols}->@*),
+ FROM => "\"$t->{base}{name}\"",
+ 'WHERE chid IN'
+ ), $entry;
+
+ while(my($name, $tbl) = each $t->{tables}->%*) {
+ $entry->{$name} = tuwf->dbAlli(
+ SELECT => sql_comma(map "\"$_->{name}\"", grep $_->{name} ne 'chid', $tbl->{cols}->@*),
+ FROM => "\"$tbl->{name}\"",
+ WHERE => { chid => $entry->{chid} }
+ );
+ }
+ $entry
+}
+
1;
diff --git a/lib/VNWeb/Docs/Lib.pm b/lib/VNWeb/Docs/Lib.pm
new file mode 100644
index 00000000..eed1afc0
--- /dev/null
+++ b/lib/VNWeb/Docs/Lib.pm
@@ -0,0 +1,90 @@
+package VNWeb::Docs::Lib;
+
+use VNWeb::Prelude;
+use Text::MultiMarkdown 'markdown';
+
+our @EXPORT = qw/md2html/;
+
+
+# Lets you call TUWF::XML functions and returns a string, doesn't affect any existing TUWF::XML outputs.
+# Nice idea for a TUWF::XML feature.
+sub lexicalxml(&) {
+ my $f = shift;
+ my $buf = '';
+ local $TUWF::XML::OBJ = TUWF::XML->new(write => sub { $buf .= shift });
+ $f->();
+ $buf
+}
+
+
+sub _moderators {
+ my $l = tuwf->dbAlli('SELECT id, username, perm FROM users WHERE (perm & ', \(auth->allPerms &~ auth->defaultPerms), ') > 0 ORDER BY id LIMIT 100');
+ my @modperms = grep 0 == (auth->listPerms->{$_} & auth->defaultPerms), keys auth->listPerms->%*;
+
+ lexicalxml {
+ dl_ sub {
+ for my $u (@$l) {
+ dt_ sub { a_ href => "/u$u->{id}", $u->{username} };
+ dd_ auth->allPerms == ($u->{perm} & auth->allPerms) ? 'admin'
+ : join ', ', sort grep $u->{perm} & auth->listPerms->{$_}, @modperms;
+ }
+ }
+ }
+}
+
+
+sub _skincontrib {
+ my %users;
+ push $users{ tuwf->{skins}{$_}[1] }->@*, [ $_, tuwf->{skins}{$_}[0] ]
+ for sort { tuwf->{skins}{$a}[0] cmp tuwf->{skins}{$b}[0] } keys tuwf->{skins}->%*;
+
+ my $u = tuwf->dbAlli('SELECT id, username FROM users WHERE id IN', [keys %users]);
+
+ lexicalxml {
+ dl_ sub {
+ for my $u (@$u) {
+ dt_ sub { a_ href => "/u$u->{id}", $u->{username} };
+ dd_ sub {
+ join_ ', ', sub { a_ href => "?skin=$_->[0]", $_->[1] }, $users{$u->{id}}->@*
+ }
+ }
+ }
+ }
+}
+
+
+sub md2html {
+ my $content = shift;
+
+ $content =~ s{^:MODERATORS:$}{_moderators}me;
+ $content =~ s{^:SKINCONTRIB:$}{_skincontrib}me;
+
+ my $html = markdown $content, {
+ strip_metadata => 1,
+ img_ids => 0,
+ disable_footnotes => 1,
+ disable_bibliography => 1,
+ };
+
+ # Number sections and turn them into links
+ my($sec, $subsec) = (0,0);
+ $html =~ s{<h([1-2])[^>]+>(.*?)</h\1>}{
+ if($1 == 1) {
+ $sec++;
+ $subsec = 0;
+ qq{<h3><a href="#$sec" name="$sec">$sec. $2</a></h3>}
+ } elsif($1 == 2) {
+ $subsec++;
+ qq|<h4><a href="#$sec.$subsec" name="$sec.$subsec">$sec.$subsec. $2</a></h4>\n|
+ }
+ }ge;
+
+ # Text::MultiMarkdown doesn't handle fenced code blocks properly. The
+ # following solution breaks inline code blocks, but I don't use those anyway.
+ $html =~ s/<code>/<pre>/g;
+ $html =~ s#</code>#</pre>#g;
+
+ $html
+}
+
+1;
diff --git a/lib/VNWeb/Docs/Page.pm b/lib/VNWeb/Docs/Page.pm
new file mode 100644
index 00000000..15d501a2
--- /dev/null
+++ b/lib/VNWeb/Docs/Page.pm
@@ -0,0 +1,67 @@
+package VNWeb::Docs::Page;
+
+use VNWeb::Prelude;
+use VNWeb::Docs::Lib;
+
+
+sub _index_ {
+ ul_ class => 'index', sub {
+ li_ sub { b_ 'Guidelines' };
+ li_ sub { a_ href => '/d5', 'Editing Guidelines' };
+ li_ sub { a_ href => '/d2', 'Visual Novels' };
+ li_ sub { a_ href => '/d15', 'Special Games' };
+ li_ sub { a_ href => '/d3', 'Releases' };
+ li_ sub { a_ href => '/d4', 'Producers' };
+ li_ sub { a_ href => '/d16', 'Staff' };
+ li_ sub { a_ href => '/d12', 'Characters' };
+ li_ sub { a_ href => '/d10', 'Tags & Traits' };
+ li_ sub { a_ href => '/d13', 'Capturing Screenshots' };
+ li_ sub { b_ 'About VNDB' };
+ li_ sub { a_ href => '/d9', 'Discussion Board' };
+ li_ sub { a_ href => '/d6', 'FAQ' };
+ li_ sub { a_ href => '/d7', 'About Us' };
+ li_ sub { a_ href => '/d17', 'Privacy Policy & Licensing' };
+ li_ sub { a_ href => '/d11', 'Database API' };
+ li_ sub { a_ href => '/d14', 'Database Dumps' };
+ li_ sub { a_ href => '/d18', 'Database Querying' };
+ li_ sub { a_ href => '/d8', 'Development' };
+ }
+}
+
+
+sub _rev_ {
+ my $d = shift;
+ revision_ d => $d, sub {},
+ [ title => 'Title' ],
+ [ content => 'Contents' ];
+}
+
+
+# A little in-memory cache of the rendered HTML for the latest revision of each
+# doc page. md2html() performance is "acceptable" for regular page loads but
+# can still feel a little sluggish.
+my %cache; # chid => html
+
+
+TUWF::get qr{/$RE{drev}} => sub {
+ my $d = db_entry d => tuwf->capture('id'), tuwf->capture('rev');
+ return tuwf->resNotFound if !$d;
+
+ my $html = $cache{$d->{chid}} || md2html $d->{content};
+ $cache{$d->{chid}} ||= $html if $d->{chrev} == $d->{maxrev};
+
+ framework_ title => $d->{title}, type => 'd', dbobj => $d, hiddenmsg => 1,
+ sub {
+ _rev_ $d if tuwf->capture('rev');
+ div_ class => 'mainbox', sub {
+ h1_ $d->{title};
+ div_ class => 'docs', sub {
+ _index_;
+ lit_ $html;
+ clearfloat_;
+ };
+ };
+ };
+};
+
+1;
diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm
index e28c7631..1e237a78 100644
--- a/lib/VNWeb/HTML.pm
+++ b/lib/VNWeb/HTML.pm
@@ -1,20 +1,32 @@
package VNWeb::HTML;
-use v5.24;
+use v5.26;
use warnings;
-use TUWF ':html5_', 'uri_escape';
+use utf8;
+use Algorithm::Diff::XS 'sdiff', 'compact_diff';
+use Encode 'encode_utf8';
+use JSON::XS;
+use TUWF ':html5_', 'uri_escape', 'html_escape', 'mkclass';
use Exporter 'import';
use JSON::XS;
use VNDB::Config;
+use VNDB::BBCode;
use VNWeb::Auth;
+use VNWeb::Validation;
+use VNWeb::DB;
+use VNDB::Func 'fmtdate';
our @EXPORT = qw/
clearfloat_
debug_
+ join_
+ user_
framework_
+ revision_
/;
+
# Ugly hack to move rendering down below the float object.
sub clearfloat_ { div_ class => 'clearfloat', '' }
@@ -28,6 +40,29 @@ sub debug_ {
}
+# Similar to join($sep, map $f->(), @list), but works for HTML generation functions.
+# join_ ', ', sub { a_ href => '#', $_ }, @list;
+# join_ \&br_, \&txt_, @list;
+sub join_($&@) {
+ my($sep, $f, @list) = @_;
+ for my $i (0..$#list) {
+ ref $sep ? $sep->() : txt_ $sep if $i > 0;
+ local $_ = $list[$i];
+ $f->();
+ }
+}
+
+
+# Display a user link.
+sub user_ {
+ my($uid, $username) = @_;
+ return lit_ '[deleted]' if !$uid;
+ a_ href => "/u$uid", $username;
+}
+
+
+
+
sub _head_ {
my $o = shift;
my $skin = tuwf->reqGet('skin') || auth->pref('skin') || config->{skin_default};
@@ -179,12 +214,90 @@ sub _footer_ {
}
+sub _maintabs_ {
+ my $opt = shift;
+ my($t, $o, $sel) = @{$opt}{qw/type dbobj tab/};
+ return if !$t || !$o;
+ return if $t eq 'g' && !auth->permTagmod;
+
+ my $id = $t.$o->{id};
+
+ my sub t {
+ my($tabname, $url, $text) = @_;
+ li_ mkclass(tabselected => $tabname eq ($sel||'')), sub {
+ a_ href => $url, $text;
+ };
+ };
+
+ ul_ class => 'maintabs', sub {
+ t hist => "/$id/hist", 'history' if $t =~ /[uvrpcsd]/;
+
+ if($t =~ /[uvp]/) {
+ my $cnt = tuwf->dbVali(q{
+ SELECT COUNT(*)
+ FROM threads_boards tb
+ JOIN threads t ON t.id = tb.tid
+ WHERE}, { 'tb.type' => $t, 'tb.iid' => $o->{id}, 't.hidden' => 0, 't.private' => 0 });
+ t disc => "/t/$id", "discussions ($cnt)";
+ };
+
+ # TODO: User lists
+
+ t tagmod => "/$id/tagmod", 'modify tags' if $t eq 'v' && auth->permTag && !$o->{entry_hidden};
+ t copy => "/$id/copy", 'copy' if $t =~ /[rc]/ && can_edit $t, $o;
+ t edit => "/$id/edit", 'edit' if can_edit $t, $o;
+ t del => "/$id/del", 'remove' if $t eq 'u' && auth && auth->uid == 2;
+ t releases => "/$id/releases", 'releases' if $t eq 'v';
+ t rgraph => "/$id/rg", 'relations' if $t =~ /[vp]/ && $o->{rgraph}; # TODO: Check DB if this field isn't given?
+ t '' => "/$id", $id;
+ }
+}
+
+
+# Returns 1 if the page contents should be hidden.
+sub _hidden_msg_ {
+ my $o = shift;
+
+ die "Can't use hiddenmsg on an object that is missing 'entry_hidden'" if !exists $o->{dbobj}{entry_hidden};
+ return 0 if !$o->{dbobj}{entry_hidden};
+
+ my $msg = tuwf->dbVali(
+ 'SELECT comments
+ FROM changes
+ WHERE', { type => $o->{type}, itemid => $o->{dbobj}{id} },
+ 'ORDER BY id DESC LIMIT 1'
+ );
+ my $board = $o->{type} =~ /[vp]/ ? $o->{type}.$o->{dbobj}{id} : 'db'; # TODO: Link to VN board for characters and releases?
+ div_ class => 'mainbox', sub {
+ h1_ $o->{title};
+ div_ class => 'warning', sub {
+ h2_ 'Item deleted';
+ p_ sub {
+ txt_ 'This item has been deleted from the database. You may file a request on the ';
+ a_ href => "/t/$board", "discussion board";
+ txt_ ' if you believe that this entry should be restored.';
+ br_;
+ br_;
+ lit_ bb2html $msg;
+ }
+ }
+ };
+ !auth->permDbmod # dbmods can still see the page
+}
+
+
# Options:
-# title => $title
-# noindex => 1/0
-# feeds => 1/0
-# search => $query
-# og => { opengraph metadata }
+# title => $title
+# noindex => 1/0
+# feeds => 1/0
+# search => $query
+# og => { opengraph metadata }
+# type => Database entry type (used for the main tabs & hidden message)
+# dbobj => Database entry object (used for the main tabs & hidden message)
+# Recognized object fields: id, entry_hidden, entry_locked
+# tab => Current tab, or empty for the main tab
+# hiddenmsg => 1/0, if true and dbobj is 'hidden', a message will be displayed
+# and the content function will not be called.
# sub { content }
sub framework_ {
my $cont = pop;
@@ -197,11 +310,210 @@ sub framework_ {
div_ id => 'header', sub { h1_ sub { a_ href => '/', 'the visual novel database' } };
div_ id => 'menulist', sub { _menu_ \%o };
div_ id => 'maincontent', sub {
- $cont->();
- div_ id => 'footer', sub { _footer_ };
+ _maintabs_ \%o;
+ $cont->() unless $o{hiddenmsg} && _hidden_msg_ \%o;
+ div_ id => 'footer', \&_footer_;
}
}
}
}
+
+
+
+sub _revision_header_ {
+ my($type, $obj) = @_;
+ b_ "Revision $obj->{chrev}";
+ if(auth) {
+ lit_ ' (';
+ a_ href => "/$type$obj->{id}.$obj->{chrev}/edit", $obj->{chrev} == $obj->{maxrev} ? 'edit' : 'revert to';
+ if($obj->{rev_requester}) {
+ lit_ ' / ';
+ a_ href => "/t/u$obj->{rev_requester}/new?title=Regarding%20$type$obj->{id}.$obj->{chrev}", 'msg user';
+ }
+ lit_ ')';
+ }
+ br_;
+ lit_ 'By ';
+ user_ @{$obj}{'rev_requester', 'rev_username'};
+ lit_ ' on ';
+ txt_ fmtdate $obj->{rev_added}, 'full';
+}
+
+
+
+sub _revision_fmtval_ {
+ my($opt, $val) = @_;
+ return i_ '[empty]' if !defined $val || !length $val;
+ return lit_ html_escape $val if !$opt->{fmt};
+ return txt_ $val ? 'True' : 'False' if $opt->{fmt} eq 'bool';
+ local $_ = $val;
+ $opt->{fmt}->();
+}
+
+
+sub _revision_fmtcol_ {
+ my($opt, $i, $l) = @_;
+
+ my $ctx = 100; # Number of characters of context in textual diffs
+ my sub sep_ { b_ class => 'standout', '<...>' }; # Context separator
+
+ td_ class => 'tcval', sub {
+ join_ $opt->{join}||\&br_, sub {
+ my($ch, $old, $new, $diff) = @$_;
+ my $val = $_->[$i];
+
+ if($diff) {
+ my $lastchunk = int (($#$diff-2)/2);
+ for my $n (0..$lastchunk) {
+ my $a = join '', @{$old}[ $diff->[$n*2] .. $diff->[$n*2+2]-1 ];
+ my $b = join '', @{$new}[ $diff->[$n*2+1] .. $diff->[$n*2+3]-1 ];
+
+ # Difference, highlight and display in full
+ if($n % 2) {
+ b_ class => $i == 1 ? 'diff_del' : 'diff_add', sub { lit_ html_escape $i == 1 ? $a : $b };
+ # Short context, display in full
+ } elsif(length $a < $ctx*3) {
+ lit_ html_escape $a;
+ # Longer context, abbreviate
+ } elsif($n == 0) {
+ sep_; br_; lit_ html_escape substr $a, -$ctx;
+ } elsif($n == $lastchunk) {
+ lit_ html_escape substr $a, 0, $ctx; br_; sep_;
+ } else {
+ lit_ html_escape substr $a, 0, $ctx;
+ br_; br_; sep_; br_; br_;
+ lit_ html_escape substr $a, -$ctx;
+ }
+ }
+
+ } elsif(@$l > 2 && $i == 2 && ($ch eq '+' || $ch eq 'c')) {
+ b_ class => 'diff_add', sub { _revision_fmtval_ $opt, $val }
+ } elsif(@$l > 2 && $i == 1 && ($ch eq '-' || $ch eq 'c')) {
+ b_ class => 'diff_del', sub { _revision_fmtval_ $opt, $val }
+ } elsif($ch eq 'c' || $ch eq 'u') {
+ _revision_fmtval_ $opt, $val;
+ }
+ }, @$l;
+ };
+}
+
+
+sub _revision_diff_ {
+ my($type, $old, $new, $field, $name, %opt) = @_;
+
+ # First do a diff on the raw field elements.
+ # (if the field is a scalar, it's considered a single element and the diff just tests equality)
+ my @old = ref $old->{$field} eq 'ARRAY' ? $old->{$field}->@* : ($old->{$field});
+ my @new = ref $new->{$field} eq 'ARRAY' ? $new->{$field}->@* : ($new->{$field});
+
+ my $JS = JSON::XS->new->utf8->allow_nonref;
+ my $l = sdiff \@old, \@new, sub { $JS->encode($_[0]) };
+ return if !grep $_->[0] ne 'u', @$l;
+
+ # Now check if we should do a textual diff on the changed items.
+ for my $item (@$l) {
+ last if $opt{fmt};
+ next if $item->[0] ne 'c' || ref $item->[1] || ref $item->[2];
+ next if !defined $item->[1] || !defined $item->[2];
+ next if length $item->[1] < 10 || length $item->[2] < 10;
+
+ # Do a word-based diff if this is a large chunk of text, otherwise character-based.
+ my $split = length $item->[1] > 1024 ? qr/([ ,\n]+)/ : qr//;
+ $item->[1] = [split $split, $item->[1]];
+ $item->[2] = [split $split, $item->[2]];
+ $item->[3] = compact_diff $item->[1], $item->[2], \&encode_utf8;
+ }
+
+ tr_ sub {
+ td_ $name;
+ _revision_fmtcol_ \%opt, 1, $l;
+ _revision_fmtcol_ \%opt, 2, $l;
+ }
+}
+
+
+sub _revision_cmp_ {
+ my($type, $old, $new, @fields) = @_;
+
+ table_ class => 'stripe', sub {
+ thead_ sub {
+ tr_ sub {
+ td_ ' ';
+ td_ sub { _revision_header_ $type, $old };
+ td_ sub { _revision_header_ $type, $new };
+ };
+ tr_ sub {
+ td_ ' ';
+ td_ colspan => 2, sub {
+ b_ "Edit summary for revision $new->{chrev}";
+ br_;
+ br_;
+ lit_ bb2html $new->{rev_comments}||'-';
+ };
+ };
+ };
+ _revision_diff_ $type, $old, $new, @$_ for(
+ [ hidden => 'Hidden', fmt => 'bool' ],
+ [ locked => 'Locked', fmt => 'bool' ],
+ @fields,
+ );
+ };
+}
+
+
+# Revision info box.
+#
+# Arguments: $type, $object, \&enrich_for_diff, @fields
+#
+# The given $object is assumed to originate from VNWeb::DB::db_entry() and
+# should have the 'id', 'hidden', 'locked', 'chrev' and 'maxrev' fields in
+# addition to those specified in @fields.
+#
+# \&enrich_for_diff is a subroutine that is given an earlier revision returned
+# by db_entry() and should enrich this object with information necessary for
+# diffing. $object is assumed to have already been enriched in this way (it is
+# assumed that a page will need to fetch and enrich such an $object for its own
+# display purposes anyway).
+#
+# @fields is a list of arrayrefs with the following form:
+#
+# [ field_name, display_name, %options ]
+#
+# Options:
+# fmt => 'bool'||sub {$_} - Formatting function for individual values.
+# If not given, the field is rendered as plain text and changes are highlighted with a diff.
+# join => sub{} - HTML to join multi-value fields, defaults to \&br_.
+sub revision_ {
+ my($type, $new, $enrich, @fields) = @_;
+
+ my $old = $new->{chrev} == 1 ? undef : db_entry $type, $new->{id}, $new->{chrev} - 1;
+ $enrich->($old) if $old;
+
+ enrich_merge chid => sql(
+ 'SELECT c.id AS chid, c.comments as rev_comments,', sql_totime('c.added'), 'as rev_added
+ , c.requester as rev_requester, u.username as rev_username
+ FROM changes c LEFT JOIN users u ON u.id = c.requester
+ WHERE c.id IN'),
+ $new, $old||();
+
+ div_ class => 'mainbox revision', sub {
+ h1_ "Revision $new->{chrev}";
+
+ a_ class => 'prev', href => sprintf('/%s%d.%d', $type, $new->{id}, $new->{chrev}-1), '<- earlier revision' if $new->{chrev} > 1;
+ a_ class => 'next', href => sprintf('/%s%d.%d', $type, $new->{id}, $new->{chrev}+1), 'later revision ->' if $new->{chrev} < $new->{maxrev};
+ p_ class => 'center', sub { a_ href => "/$type$new->{id}", $type.$new->{id} };
+
+ div_ class => 'rev', sub {
+ _revision_header_ $type, $new;
+ br_;
+ b_ 'Edit summary';
+ br_; br_;
+ lit_ bb2html $new->{rev_comments}||'-';
+ } if !$old;
+
+ _revision_cmp_ $type, $old, $new, @fields if $old;
+ };
+}
+
1;
diff --git a/lib/VNWeb/Prelude.pm b/lib/VNWeb/Prelude.pm
new file mode 100644
index 00000000..ecc5a606
--- /dev/null
+++ b/lib/VNWeb/Prelude.pm
@@ -0,0 +1,76 @@
+# Importing this module is equivalent to:
+#
+# use v5.26;
+# use warnings;
+# use utf8;
+#
+# use TUWF ':html5_', 'mkclass';
+# use Exporter 'import';
+# use Time::HiRes 'time';
+#
+# use VNDBUtil;
+# use VNDB::Types;
+# use VNDB::Config;
+# use VNWeb::Auth;
+# use VNWeb::HTML;
+# use VNWeb::DB;
+#
+# WARNING: This should not be used from the above modules.
+package VNWeb::Prelude;
+
+use strict;
+use warnings;
+use feature ':5.26';
+use utf8;
+
+sub import {
+ my $c = caller;
+
+ strict->import;
+ warnings->import;
+ feature->import(':5.26');
+ utf8->import;
+
+ die $@ if !eval <<" EOM;";
+ package $c;
+
+ use TUWF ':html5_', 'mkclass';
+ use Exporter 'import';
+ use Time::HiRes 'time';
+
+ use VNDBUtil;
+ use VNDB::Types;
+ use VNDB::Config;
+ use VNWeb::Auth;
+ use VNWeb::HTML;
+ use VNWeb::DB;
+ 1;
+ EOM;
+
+ no strict 'refs';
+ *{$c.'::RE'} = *RE;
+}
+
+
+# Regular expressions for use in path registration
+my $num = qr{[1-9][0-9]{0,6}};
+my $id = qr{(?<id>$num)};
+my $rev = qr{(?:\.(?<rev>$num))};
+our %RE = (
+ uid => qr{u$id},
+ vid => qr{v$id},
+ rid => qr{r$id},
+ sid => qr{s$id},
+ cid => qr{c$id},
+ pid => qr{p$id},
+ iid => qr{i$id},
+ did => qr{d$id},
+ vrev => qr{v$id$rev?},
+ rrev => qr{r$id$rev?},
+ prev => qr{p$id$rev?},
+ srev => qr{s$id$rev?},
+ crev => qr{c$id$rev?},
+ drev => qr{d$id$rev?},
+);
+
+1;
diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm
new file mode 100644
index 00000000..a014b11d
--- /dev/null
+++ b/lib/VNWeb/Validation.pm
@@ -0,0 +1,26 @@
+package VNWeb::Validation;
+
+use v5.26;
+use TUWF;
+use VNWeb::Auth;
+use Exporter 'import';
+
+our @EXPORT = qw/
+ can_edit
+/;
+
+
+# Returns whether the current user can edit the given database entry.
+sub can_edit {
+ my($type, $entry) = @_;
+
+ return auth->permUsermod || (auth && $entry->{id} == auth->uid) if $type eq 'u';
+ return auth->permDbmod if $type eq 'd';
+
+ die "Can't do authorization test when entry_hidden/entry_locked fields aren't present"
+ if $entry->{id} && (!exists $entry->{entry_hidden} || !exists $entry->{entry_locked});
+
+ auth->permDbmod || (auth->permEdit && !($entry->{entry_hidden} || $entry->{entry_locked}));
+}
+
+1;