# Simple Photo Gallery: Web Interface
# (c) 2003--2021 Martin Mares <mj@ucw.cz>

package UCW::Gallery::Web;

use common::sense;

use UCW::Gallery;
use UCW::CGI;
use Digest::SHA;
use File::Spec;

my $show_img;
my $want_archive;
my $auth_password;

my %args = (
	'i'	=> { 'var' => \$show_img, 'check' => '\d+' },
	'a'	=> { 'var' => \$want_archive },
	'pw'	=> { 'var' => \$auth_password },
);

sub error($) {
	print "<p style='color:red'>Bad luck, the script is broken. Sorry.\n<p>$_[0]\n";
	print "</body></html>\n";
}

sub get($$) {
	my ($self, $key) = @_;
	return $self->{gal}->get($key);
}

sub try_get($$) {
	my ($self, $key) = @_;
	return $self->{gal}->try_get($key);
}

sub extras($$) {
	my ($self, $key) = @_;
	my $val = $self->get($key);
	if (ref $val eq 'CODE') {
		return &$val($self);
	} else {
		return $val;
	}
}

# For use by extras hooks
sub gallery($) {
	my ($self) = @_;
	return $self->{gal};
}

# For use by extras hooks: return true if we are showing an image page, false for index page
sub showing_image($) {
	my ($self) = @_;
	return $show_img ne "";
}

sub html_top($) {
	my ($self) = @_;
	my $title = $self->get('Title');
	my $hextras = $self->extras('WebHeadExtras');
	my $theme_hextras = $self->theme_head_extras;
	print <<EOF ;
Content-Type: text/html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
$hextras$theme_hextras<title>$title</title>
</head><body>
EOF

	$UCW::CGI::ErrorHandler::error_hook = \&error;

	# WebTopExtras are evaluated separately, since they can override the error hook
	print $self->extras('WebTopExtras');
}

sub html_bot($) {
	my ($self) = @_;
	print $self->extras('WebBotExtras'), "</body></html>\n";
}

sub show_img($) {
	my ($self) = @_;

	if ($show_img < 1 || $show_img > $self->{num_photos}) {
		UCW::CGI::http_error('404 No such photo');
		return;
	}

	my $meta = $self->{meta};
	my $id = $meta->{sequence}->[$show_img-1];
	my $m = $meta->{photo}->{$id} or die;
	$self->html_top;

	$self->show_links(($show_img > 1 ? ("?i=".($show_img-1)) : ""),
			  ".",
			  ($show_img < $self->{num_photos} ? ("?i=".($show_img+1)) : ""));

	my $t = UCW::CGI::html_escape($m->{title});
	my $w = $m->{w};
	my $h = $m->{h};
	print "<h1>$t</h1>\n" if $t ne "";
	my $img = $self->get('PhotoUrlPrefix') . $self->{gal}->photo_file_name($m, $id);
	print "<p class=large><img src='$img' width=$w height=$h alt='$t'>\n";

	$self->html_bot;
}

sub show_pre_thumbs($) {
	my ($self) = @_;
}

sub show_post_thumbs($) {
	my ($self) = @_;
}

sub show_common_header($) {
	my ($self) = @_;
	$self->html_top;

	$self->show_links($self->get('BackURL'), $self->get('ParentURL'), $self->get('FwdURL'));
	print "<h1>", $self->get('Title'), "</h1>\n";
	my $subtitle = $self->get('SubTitle');
	print "<h2>$subtitle</h2>\n" if $subtitle ne "";
}

sub show_list($) {
	my ($self) = @_;
	$self->show_common_header;
	$self->show_pre_thumbs;

	my $meta = $self->{meta};
	for my $idx (1..$self->{num_photos}) {
		my $id = $meta->{sequence}->[$idx-1];
		my $m = $meta->{photo}->{$id};
		my $click_url;
		if ($self->get('WebImageSubpages')) {
			$click_url = "?i=$idx";
		} else {
			$click_url = $self->get('PhotoUrlPrefix') . $self->{gal}->photo_file_name($m, $id);
		}
		$self->show_thumb($meta, $id, $click_url);
	}

	$self->show_post_thumbs;
	$self->html_bot();
}

sub show_login_page($$) {
	my ($self, $login_failed) = @_;

	$self->show_common_header;

	print "<div class='gal-login'>\n";
	print $self->extras('WebLoginExtras');

	my $wrong = $login_failed ? " class='gal-login-bad'" : "";
	print "\t<form method=POST action='.'>\n";
	print "\t\t<input type=password name=pw$wrong>\n";
	print "\t\t<input type=submit value='Login'>\n";
	print "\t</form>\n";

	print "</div>\n";
	$self->html_bot;
}

sub auth_check($) {
	my ($self) = @_;

	my $needed = $self->auth_get_needed;
	@$needed or return 1;

	if (length $auth_password) {
		my $passwords = $self->get('AuthPasswords');
		my $match = 0;
		for my $zone (@$needed) {
			if (defined $passwords->{$zone} && $passwords->{$zone} eq $auth_password) {
				my @opts = ();
				my $path = $self->try_get('AuthCookiePath');
				push @opts, 'path', $path if defined $path;
				push @opts, 'secure', undef if $self->get('AuthCookieSecure');
				UCW::CGI::set_cookie($self->get('AuthCookiePrefix') . $zone, $self->auth_zone_hash($zone), @opts);
				$match++;
			}
		}

		if ($match) {
			my $abs = $self->try_get('WebAbsURL');
			if (defined $abs) {
				print "Status: 303\n";
				print "Location: $abs\n\n";
				exit 0;
			}
			return 1;
		}

		$self->show_login_page($match == 0);
		return;
	}

	my $known_tokens = $self->auth_parse_cookies;
	for my $zone (@$needed) {
		return 1 if $known_tokens->{$zone};
	}

	$self->show_login_page(0);
	return;
}

sub auth_get_needed($) {
	my ($self) = @_;

	my $auth = $self->get('AuthNeeded');
	defined $auth or return [];
	if (ref $auth) {
		return $auth;
	} else {
		$auth ne "" or return [];
		return [$auth];
	}
}

sub auth_parse_cookies($) {
	my ($self) = @_;
	my $known_tokens = {};

	my %cookies = UCW::CGI::parse_cookies;
	my $px = $self->get('AuthCookiePrefix');
	for my $k (keys %cookies) {
		if (substr($k, 0, length $px) eq $px) {
			my $zone = substr($k, length $px);
			my $v = $cookies{$k};
			if ($v eq $self->auth_zone_hash($zone)) {
				$known_tokens->{$zone} = 1;
			} else {
				print STDERR "Gallery: Invalid auth cookie for zone $zone\n";
			}
		}
	}

	return $known_tokens;
}

sub auth_zone_hash($) {
	my ($self, $zone) = @_;
	my $secret = $self->get('AuthSecret');
	return substr(Digest::SHA::hmac_sha256_hex($zone, $secret), 0, 16);
}

sub dispatch($) {
	my ($self) = @_;
	binmode STDOUT, ':utf8';
	UCW::CGI::parse_args(\%args);

	$self->auth_check or return;

	$self->{meta} = $self->{gal}->read_meta(File::Spec->catfile($self->get('CacheDir'), 'cache.meta'));
	$self->{num_photos} = scalar @{$self->{meta}->{sequence}};

	if ($want_archive) {
		require UCW::Gallery::Archive;
		UCW::Gallery::Archive::send_archive($self->{gal}, $self->{meta});
	} elsif ($show_img ne "") {
		$self->show_img;
	} else {
		$self->show_list;
	}
}

sub attach($$) {
	my ($class, $gal) = @_;
	my $self = { gal => $gal };
	$gal->def(
		WebFE => $self,

		# Extras are either strings or functions called with the current gallery object as parameter
		WebHeadExtras => "",
		WebTopExtras => "",
		WebBotExtras => "",
		WebLoginExtras => "",

		# Used by the theming logic
		WebThemeCSS => undef,

		# 1 if thumbnail link to sub-pages with images, 0 if they link directly to image files
		WebImageSubpages => 1,

		# If enabled, calling the CGI with a=zip produces a ZIP archive with all photos.
		WebAllowArchives => 1,

		# Optional absolute URL of the current gallery. This is useful for redirects after login.
		# If it is not set, form POST does not redirect to GET.
		WebAbsURL => undef,

		# Authentication: Known zones and their passwords.
		# AuthPasswords => { 'zone' => 'passwd' },

		# Authentication: Which zones have access to the current gallery.
		AuthNeeded => [],

		# Authentication: Secret used for encryption of cookies.
		# AuthSecret => "",

		# Authentication: Properties of cookies.
		AuthCookiePrefix => 'gal_',
		AuthCookiePath => undef,
		AuthCookieSecure => 1,
	);
	bless $self, $class;
	return $self;
}

42;
