فهرست منبع

Make tCMS a chrooted system service, and much more

* Add support for "presentation" type posts
* Add makefile targets to set up:
  - pdns
  - mail
  - firewall

And more!
George Baugh 2 سال پیش
والد
کامیت
73181e4221

+ 13 - 0
.gitignore

@@ -40,3 +40,16 @@ dns/default.zone
 dns/default.zone.sql
 *-shm
 *-wal
+service-files/*.service
+.ssh/*
+.bash_profile
+.bash_history
+.bashrc
+.cache/
+.gitconfig
+.viminfo
+.vimrc
+.tmux.conf
+.tig_history
+.sqlite_history
+.xslate_cache

+ 47 - 18
.perltidyrc

@@ -1,5 +1,6 @@
 SHELL := /bin/bash
 SERVER_NAME := $(shell bin/tcms-hostname)
+USER_NAME   := $(shell bin/tcms-hostname --user)
 
 .PHONY: depend
 depend:
@@ -17,15 +18,33 @@ install:
 	test -d logs/ && mkdir -p logs/; /bin/true
 	$(RM) pod2htmd.tmp;
 
+.PHONY: service-user
+service-user:
+	sudo useradd -MU -s /sbin/nologin -d "$(shell pwd)" $(USER_NAME); /bin/true
+	sudo chown -R $(USER_NAME):$(USER_NAME) .
+	# Can be 760 if you aren't using git features as a developer user that is not the system user.
+	sudo chmod -R 0770 .
+	# For some reason, nginx needs world readability to see the socket, despite having group permissions.
+	# Seems pretty dumb to me, but whatever.  We are locking every single other file away from it & other users.
+	sudo chmod 0755 .
+	sudo chmod 0775 run
+	sudo chown -R $(USER_NAME):www-data run
+	sudo chmod -R 0770 bin/ tcms www/server.psgi
+	sudo -u $(USER_NAME) mkdir 0700 .ssh
+	sudo -u $(USER_NAME) touch .ssh/authorized_keys
+	sudo -u $(USER_NAME) chmod 0600 .ssh/authorized_keys
+
 .PHONY: install-service
-install-service:
-	mkdir -p ~/.config/systemd/user
-	cp service-files/systemd.unit ~/.config/systemd/user/tCMS.service
-	sed -ie 's#__REPLACEME__#$(shell pwd)#g' ~/.config/systemd/user/tCMS.service
-	systemctl --user daemon-reload
-	systemctl --user enable tCMS
-	systemctl --user start tCMS
-	loginctl enable-linger $(USER)
+install-service: #service-user
+	sudo systemctl disable $(SERVER_NAME); /bin/true
+	cp service-files/systemd.unit service-files/$(SERVER_NAME).service
+	sed -i 's#__DOMAIN__#$(SERVER_NAME)#g' service-files/$(SERVER_NAME).service
+	sed -i 's#__USER__#$(USER_NAME)#g' service-files/$(SERVER_NAME).service
+	sed -i 's#__REPLACEME__#$(shell pwd)#g' service-files/$(SERVER_NAME).service
+	sudo ln -sr service-files/$(SERVER_NAME).service /usr/lib/systemd/system/$(SERVER_NAME).service; /bin/true
+	sudo systemctl daemon-reload
+	sudo systemctl enable $(SERVER_NAME)
+	sudo systemctl start $(SERVER_NAME)
 
 .PHONY: prereq-debian
 prereq-debian: prereq-debs prereq-perl prereq-frontend prereq-node
@@ -57,12 +76,13 @@ prereq-frontend:
 		"https://raw.githubusercontent.com/chalda-pnuzig/emojis.json/master/dist/list.min.json" \
 		"https://raw.githubusercontent.com/highlightjs/cdn-release/main/build/highlight.min.js" \
 		"https://cdn.jsdelivr.net/npm/chart.js" \
-		"https://github.com/hakimel/reveal.js/blob/master/dist/reveal.js"; popd
+		"https://raw.githubusercontent.com/hakimel/reveal.js/master/dist/reveal.js"; popd
 	mkdir -p www/styles; pushd www/styles && curl -L --remote-name-all \
 		"https://raw.githubusercontent.com/highlightjs/cdn-release/main/build/styles/obsidian.min.css" \
 	    "https://raw.githubusercontent.com/hakimel/reveal.js/master/dist/reveal.css" \
 		"https://raw.githubusercontent.com/hakimel/reveal.js/master/dist/theme/white.css"; popd
 	mv www/styles/white.css www/styles/reveal-white.css
+	sed -i 's/Source Sans Pro,//g' www/styles/reveal-white.css
 
 .PHONY: reset
 reset: reset-remove install
@@ -81,11 +101,11 @@ reset-remove:
 fail2ban:
 	cp fail2ban/tcms-jail.tmpl fail2ban/tcms-jail.conf
 	sed -i 's#__LOGDIR__#$(shell pwd)#g' fail2ban/tcms-jail.conf
-	sed -i 's#__DOMAIN__#$(shell bin/tcms-hostname)#g' fail2ban/tcms-jail.conf
-	sudo rm /etc/fail2ban/jail.d/$(shell bin/tcms-hostname).conf; /bin/true
-	sudo rm /etc/fail2ban/filter.d/$(shell bin/tcms-hostname).conf; /bin/true
-	sudo ln -sr fail2ban/tcms-jail.conf   /etc/fail2ban/jail.d/$(shell bin/tcms-hostname).conf
-	sudo ln -sr fail2ban/tcms-filter.conf /etc/fail2ban/filter.d/$(shell bin/tcms-hostname).conf
+	sed -i 's#__DOMAIN__#$(SERVER_NAME)#g' fail2ban/tcms-jail.conf
+	sudo rm /etc/fail2ban/jail.d/$(SERVER_NAME).conf; /bin/true
+	sudo rm /etc/fail2ban/filter.d/$(SERVER_NAME).conf; /bin/true
+	sudo ln -sr fail2ban/tcms-jail.conf   /etc/fail2ban/jail.d/$(SERVER_NAME).conf
+	sudo ln -sr fail2ban/tcms-filter.conf /etc/fail2ban/filter.d/$(SERVER_NAME).conf
 	sudo systemctl reload fail2ban
 
 .PHONY: nginx
@@ -95,7 +115,7 @@ nginx:
 	sed 's/\%SERVER_SOCK\%/$(shell pwd)/g' nginx/tcms.conf.intermediate > nginx/tcms.conf
 	rm nginx/tcms.conf.intermediate
 	mkdir run
-	chown $(USER):www-data run
+	chown $(USER_NAME):www-data run
 	chmod 0770 run
 	sudo mkdir -p '/var/www/$(SERVER_NAME)'
 	sudo mkdir -p '/var/www/mail.$(SERVER_NAME)'
@@ -180,8 +200,10 @@ dns:
 	# Don't need no bind
 	[[ -e /etc/powerdns/pdns.d/bind.conf ]] && sudo rm /etc/powerdns/pdns.d/bind.conf
 	# Fix broken service configuration
-	sudo bin/configure_pdns
-	sudo cp dns/10-powerdns.conf /etc/rsyslog.d/10-powerdns.conf 
+	sudo dns/configure_pdns
+	sudo chown $(USER_NAME):pdns dns/
+	sudo chown $(USER_NAME):pdns dns/zones.db
+	sudo cp dns/10-powerdns.conf /etc/rsyslog.d/10-powerdns.conf
 	sudo systemctl daemon-reload
 	sudo service rsyslog restart
 	sudo service pdns enable
@@ -191,5 +213,12 @@ dns:
 githook:
 	cp git-hooks/pre-commit .git/hooks
 
+.PHONY: firewall
+firewall:
+	# Remove dopey unauthenticated port for git from /etc/services
+	sudo sed -i '/^git\s/d' /etc/services
+	sudo cp ufw/git ufw/pdns_server /etc/ufw/applications.d
+	sudo ufw/setup-rules
+
 .PHONY: all
-all: prereq-debian install fail2ban nginx mail dns githook
+all: prereq-debian install fail2ban nginx mail dns firewall githook

+ 17 - 18
LICENSE

@@ -13,16 +13,28 @@ Deployment is currently:
 Then:
 * open tmux or screen
 * `sudo ./tcms`
-OR (if you want tCMS as a systemd service for the current user):
-* `make install-service`
+OR (if you want tCMS as a systemd service for production/preproduction contexts):
+* `make -f Installer.mk all`
 
 This sets up nginx, reverse proxy and SSL certs for you.
-You must set up the user which runs tCMS to have the primary group www-data if you want to be able to run without sudo or run as a usermode service.
+It also sets up the mailserver and DNS for you via pdns.
+
 It is strongly suggested that you chmod everything but the run/ directory to be 0700, particularly in a shared environment.
 
-It also sets up the mailserver and DNS for you.
+## Administration/Development
+
+Administrating the server should in general be done via the system user we setup which will be the domain setup with `tcms-hostname` with dots replaced with dashes.
+Slap in the authorized public key to .ssh/authorized\_keys, as this is the system user's homedir.
+From there you'll need to `sudo chsh -s /bin/bash $system_user_name` to allow logging in remotely.
+In production you should probably leave things nologin, and instead sudo into the user for shells from an administrator account.
+
+The user guide is self-hosted; After you first login, hit the 'Manual' section in the backend.
+
+Rate-Limiting is expected to be handled at the level of the webserver proxying requests to this application.
+See ufw/setup-rules as an example of the easy way to setup rules/limiting for all the services you need to run tCMS.
 
-You should add the pdns group to the user you use to run tCMS, so that the zone management features will work.
+Containerization
+====================
 
 A Dockerfile and deployment scripts are provided for your convenience in building/running containers based on this:
 ```
@@ -35,11 +47,6 @@ A Dockerfile and deployment scripts are provided for your convenience in buildin
 ```
 There is also podman container code; see images/README.md
 
-The user guide is self-hosted; After you first login, hit the 'Manual' section in the backend.
-
-Rate-Limiting is expected to be handled at the level of the webserver proxying requests to this application.
-See ufw/setup-rules as an example of the easy way to setup rules/limiting for all the services you need to run tCMS.
-
 Migration of tCMS1 sites
 =========================
 
@@ -91,14 +98,6 @@ Planned Development:
 * Elasticsearch - Documents are ideally indexed in a search engine, should be nice and fast too.
 * Git - More for the APE crossover
 
-Supported PSGI servers
-======================
-
-Starman and uWSGI
-
-In production, I would expect you to run under uWSGI, and the `tcms-uwsgi` command in the TLD runs this.
-Otherwise, you can run `tcms` to start starman normally.
-
 Ideas to come:
 =============
 

+ 12 - 0
bin/tcms-hostname

@@ -6,6 +6,12 @@ use warnings;
 use FindBin::libs;
 
 use Trog::Config();
+use Getopt::Long qw{GetOptionsFromArray};
+
+my $user;
+GetOptionsFromArray(\@ARGV,
+    'user' => \$user,
+);
 
 my $hostname = $ARGV[0];
 
@@ -18,4 +24,10 @@ if ($hostname) {
 my $domain = $conf->param('general.hostname');
 die "Hostname not set in tCMS configuration.  Please set this first by passing the hostname to bin/tcms-hostname." unless $domain;
 
+# Transform the domain name into something maximally compatible with being a username.
+if ($user) {
+    $domain = substr($domain, 0, 32);
+    $domain =~ s/\./-/g;
+}
+
 print "$domain\n";

+ 75 - 0
bin/tcms_monitor

@@ -0,0 +1,75 @@
+#!/usr/bin/env perl
+
+use FindBin::libs;
+use FindBin;
+use Proc::Daemon;
+
+use strict;
+use warnings;
+
+use Trog::Autoreload;
+
+# Note that we do not reload ourselves.
+
+sub cleanup {
+    unlink 'run/monitor.pid';
+    return 0;
+};
+
+$SIG{HUP} = sub {
+    print "Recieved signal from tcms master process, restarting.\n";
+    cleanup();
+    exec $0;
+};
+
+sub get_master_pid {
+    # Get the PID of the current master process
+    my $ppf = 'run/tcms.pid';
+    my $ppid;
+    if (-f $ppf) {
+        open(my $pidfile, '<', $ppf);
+        read($pidfile, $ppid, 10000);
+        close $pidfile;
+        chomp $ppid;
+        if (!-d "/proc/$ppid") {
+            print "tCMS not running, but left a PIDFile behind. Exiting monitor.\n";
+            exit 0 if -f "/run/$ppid";
+        }
+    } else {
+        print "tCMS not running, exiting monitor.\n";
+        exit 0;
+    }
+    return $ppid;
+}
+
+sub main {
+    my $pf = 'run/monitor.pid';
+    my $monitor_pid = Trog::Autoreload::monitor_pid();
+    if ($monitor_pid) {
+        print "Change monitor already running, killing and re-starting.\n";
+        kill 'TERM', $monitor_pid;
+    }
+
+    # daemonize immediately.  We are handling the pidfile ourselves, see below
+    my $daemon = Proc::Daemon->new(
+        work_dir => "$FindBin::Bin/..",
+        pid_file => $pf,
+        child_STDOUT => 'logs/monitor.log',
+        child_STDERR => 'logs/monitor.err',
+    );
+    my $child_pid = $daemon->Init;
+    return 0 if $child_pid;
+
+    my $ppid = get_master_pid();
+    # If we go here, we (probably) have a PID, so watch for the changes.
+    my $status = Trog::Autoreload::watch_for_changes('lib', $ppid);
+    if ($status) {
+        # tCMS process went away.  Bye bye!
+        print "tCMS process went away, shutting down monitor\n";
+        return cleanup();
+    }
+    # By now we should have recieved SIGHUP and are already dead, so exit anyways.
+    return cleanup();
+}
+
+exit main(@ARGV) unless caller;

+ 14 - 10
images/README.md

@@ -71,7 +71,6 @@ sub _app {
     # Make sure all writes are with the proper permissions, none need know of our love
     umask 0077;
 
-    INFO("TCMS starting up on PID $MASTER_PID, Worker PID $$");
     # Start the server timing clock
     my $start = [gettimeofday];
 
@@ -89,6 +88,8 @@ sub _app {
 
     # Setup logging
     log_init();
+    INFO("tCMS worker starting on pid $$");
+
     my $requestid = Trog::Utils::uuid();
     Trog::Log::uuid($requestid);
 
@@ -123,7 +124,7 @@ sub _app {
     # Check eTags.  If we don't know about it, just assume it's good and lazily fill the cache
     # XXX yes, this allows cache poisoning...but only for logged in users!
     if ( $env->{HTTP_IF_NONE_MATCH} ) {
-        INFO("$env->{REQUEST_METHOD} 304 $fullpath");
+        INFO("$env->{REQUEST_METHOD} 304 0 $fullpath");
         return [ 304, [], [''] ] if $env->{HTTP_IF_NONE_MATCH} eq ( $etags{ $env->{REQUEST_URI} } || '' );
         $etags{ $env->{REQUEST_URI} } = $env->{HTTP_IF_NONE_MATCH} unless exists $etags{ $env->{REQUEST_URI} };
     }
@@ -341,7 +342,9 @@ sub _app {
         die "$path returned no data!" unless ref $output eq 'ARRAY' && @$output == 3;
 
         my $pport = defined $query->{port} ? ":$query->{port}" : "";
-        INFO("$env->{REQUEST_METHOD} $output->[0] $fullpath");
+        my %headers = @{$output->[1]};
+        my $bytes = $headers{'Content-Length'};
+        INFO("$env->{REQUEST_METHOD} $output->[0] $bytes $fullpath");
 
         # Append server-timing headers if they aren't present
         my $tot = tv_interval($start) * 1000;
@@ -414,30 +417,31 @@ sub _generic ( $type, $query ) {
     return $lookup{$type}->($query);
 }
 
+# TODO: we need a better linkage between bytes and the message actually sent
 sub _notfound ($query) {
-    INFO("$query->{method} 404 $query->{fullpath}");
+    INFO("$query->{method} 404 33 $query->{fullpath}");
     return _generic( 'notfound', $query );
 }
 
 sub _forbidden ($query) {
-    INFO("$query->{method} 403 $query->{fullpath}");
+    INFO("$query->{method} 403 23 $query->{fullpath}");
     return _generic( 'forbidden', $query );
 }
 
 sub _badrequest ($query) {
-    INFO("$query->{method} 400 $query->{fullpath}");
+    INFO("$query->{method} 400 11 $query->{fullpath}");
     return _generic( 'badrequest', $query );
 }
 
 sub _toolong ($query) {
-    INFO("$query->{method} 419 $query->{fullpath}");
+    INFO("$query->{method} 419 12 $query->{fullpath}");
     return _generic( 'toolong', {} );
 }
 
 sub _error ($query) {
     $query->{method}   //= "UNKNOWN";
     $query->{fullpath} //= $query->{route} // '/?';
-    INFO("$query->{method} 500 $query->{fullpath}");
+    INFO("$query->{method} 500 21 $query->{fullpath}");
     return _generic( 'error', $query );
 }
 
@@ -471,7 +475,7 @@ sub _static ( $fullpath, $path, $start, $streaming, $last_fetch = 0 ) {
         # starman by comparison doesn't violate the principle of least astonishment here.
         # This is probably a performance optimization, but makes the kind of micromanagement I need to do inconvenient.
         # As such, we will just return a stream.
-        INFO("GET 200 $fullpath");
+        INFO("GET 200 $headers_parsed->{'Content-Length'} $fullpath");
 
         return sub {
             my $responder = shift;
@@ -488,7 +492,7 @@ sub _static ( $fullpath, $path, $start, $streaming, $last_fetch = 0 ) {
 
         return [ $code, [%$headers_parsed], $fh ];
     }
-    INFO("GET 403 $fullpath");
+    INFO("GET 403 23 $fullpath");
     return [ 403, [ 'Content-Type' => $Trog::Vars::content_types{text} ], ["STAY OUT YOU RED MENACE"] ];
 }
 

+ 38 - 11
lib/Theme.pm

@@ -3,34 +3,61 @@ package Trog::Autoreload;
 use strict;
 use warnings;
 
+no warnings qw{experimental};
 use feature qw{signatures};
 
+use Cwd qw{abs_path};
+use Time::HiRes qw{usleep};
 use Linux::Perl::inotify;
 
 use Trog::Utils;
 
-sub watch_for_changes ( $dir, $interval=5 ) {
-    my $inf = Linux::Perl::inotify->new([qw{NONBLOCK}]);
- 
-    my @dirs = ($dir);
+sub watch_for_changes ( $dir, $pid=undef ) {
+    my @dirs = (abs_path($dir));
     my @wds;
+
+    my $inf = Linux::Perl::inotify->new(flags => [qw{NONBLOCK}]);
     foreach my $directory (@dirs) {
         # Recursive scan for directories and setting up inotifies
         push(@dirs, _readdir( $directory ));
-        DEBUG("Watching $directory for changes");
+        print "Watching $directory for changes\n";
         push(@wds, $inf->add( path => $directory, events => [qw{CREATE MODIFY}] ));
     }
-    while (!$inf->read()) {
-        sleep $interval;
+    my @result;
+    CHECK: while (1) {
+        @result = $inf->read();
+        foreach my $res (@result) {
+            last CHECK if $res->{name} =~ m/\.pm$/;
+        }
+        usleep 100000; #100ms oughtta be enough
+        # If we know the pid, stop watching when it dies.
+        if ($pid && !-d "/proc/$pid") {
+            foreach my $wd (@wds) { $inf->remove($wd) }
+            return 1;
+        }
     }
-    INFO("Change in $dir detected");
-    Trog::Utils::restart_parent();
-    exit 0;
+    print "Relevant Change in $dir detected, reloading\n";
+    Trog::Utils::restart_parent($pid);
+    foreach my $wd (@wds) { $inf->remove($wd) }
+    return 0;
 }
 
 sub _readdir ( $dir ) {
     opendir(my $dh, $dir);
-    my @dirs = grep { -d $_ && !m/^\.+$/ } readdir($dh);
+    my @dirs = map { "$dir/$_" } grep { -d "$dir/$_" && !m/^\.+$/ } readdir($dh);
     closedir($dh);
     return @dirs;
 }
+
+sub monitor_pid {
+    my $pf = 'run/monitor.pid';
+    if (-f $pf) {
+        open(my $pidfile, '<', $pf);
+        read($pidfile, my $pid, 10000);
+        close $pidfile;
+        return $pid if -d "/proc/$pid";
+    }
+    return 0;
+}
+
+1;

+ 28 - 7
lib/Trog/Component/EmojiPicker.pm

@@ -9,6 +9,7 @@ use List::Util;
 use File::Copy;
 use Path::Tiny();
 use Ref::Util();
+use File::Basename qw{basename};
 
 use Trog::Log qw{:all};
 use Trog::Utils;
@@ -285,6 +286,11 @@ my $hashref_or_string = sub {
     return Ref::Util::is_hashref($subj) || $not_ref->($subj);
 };
 
+my $arrayref_or_string = sub {
+    my $subj = shift;
+    return Ref::Util::is_arrayref($subj) || $not_ref->($subj);
+};
+
 # TODO more strict validation of strings?
 our %schema = (
     ## Parameters which must be in every single post
@@ -302,7 +308,7 @@ our %schema = (
     'local_href' => $not_ref,
 
     # Post body
-    'data' => $not_ref,
+    'data' => $arrayref_or_string,
 
     # How do I edit this post?
     'form' => $not_ref,
@@ -335,9 +341,10 @@ our %schema = (
     'preview'      => $not_ref,
 
     ## Content specific parameters
-    'audio_href' => $not_ref,
-    'video_href' => $not_ref,
-    'file'       => $hashref_or_string,
+    'audio_href'  => $not_ref,
+    'video_href'  => $not_ref,
+    'file'        => $hashref_or_string,
+    'attachments' => \&Ref::Util::is_arrayref,
 );
 
 sub add ( $self, @posts ) {
@@ -404,7 +411,8 @@ sub _process ($post) {
     $post->{href}      = _handle_upload( $post->{file},           $post->{id} ) if $post->{file};
     $post->{preview}   = _handle_upload( $post->{preview_file},   $post->{id} ) if $post->{preview_file};
     $post->{wallpaper} = _handle_upload( $post->{wallpaper_file}, $post->{id} ) if $post->{wallpaper_file};
-    $post->{preview}   = $post->{href} if $post->{app} && $post->{app} eq 'image';
+    @{ $post->{attachments} } = map { _handle_upload( $_, $post->{id} ) } @{ $post->{attachments} } if $post->{attachments};
+    $post->{preview} = $post->{href} if $post->{app} && $post->{app} eq 'image';
     delete $post->{app};
     delete $post->{file};
     delete $post->{preview_file};
@@ -449,11 +457,24 @@ sub _process ($post) {
     return $post;
 }
 
+# Browsers try and save time in the event that the same file is sent twice, so handle that.
+my %seen_files;
+
 sub _handle_upload ( $file, $uuid ) {
-    my $f       = $file->{tempname};
+    my $fname;
+    if ( ref $file ne 'HASH' ) {
+        use Data::Dumper;
+        $file =~ s/\\/\//g;
+        $fname = basename($file);
+        FATAL("Did not expect bogus file before its real counterpart!") unless $seen_files{$fname};
+        return $seen_files{$fname};
+    }
+    my $f = $file->{tempname};
+    $fname = basename( $file->{filename} );
     my $newname = "$uuid.$file->{filename}";
     File::Copy::move( $f, "www/assets/$newname" );
-    return "/assets/$newname";
+    $seen_files{$fname} = "/assets/$newname";
+    return $seen_files{$fname};
 }
 
 =head2 delete(@posts)

+ 5 - 4
lib/Trog/Email.pm

@@ -26,7 +26,6 @@ sub serve ( $fullpath, $path, $start, $streaming, $ranges, $last_fetch = 0, $def
 
     my $ct      = 'Content-type';
     my @headers = ( $ct => $ft );
-    DEBUG("$ct : $ft");
 
     #TODO use static Cache-Control for everything but JS/CSS?
     push( @headers, 'Cache-control' => $Trog::Vars::cache_control{revalidate} );
@@ -61,6 +60,7 @@ sub serve ( $fullpath, $path, $start, $streaming, $ranges, $last_fetch = 0, $def
         #Return data in the event the caller does not support deflate
         if ( !$deflate ) {
             push( @headers, "Content-Length" => $sz );
+            INFO("GET 200 $sz $fullpath");
 
             # Append server-timing headers
             my $tot = tv_interval($start) * 1000;
@@ -76,7 +76,7 @@ sub serve ( $fullpath, $path, $start, $streaming, $ranges, $last_fetch = 0, $def
         print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
         push( @headers, "Content-Length" => length($dfh) );
 
-        INFO("GET 200 $fullpath");
+        INFO("GET 200 ".length($dfh)." $fullpath");
 
         # Append server-timing headers
         my $tot = tv_interval($start) * 1000;
@@ -85,7 +85,7 @@ sub serve ( $fullpath, $path, $start, $streaming, $ranges, $last_fetch = 0, $def
         return [ $code, \@headers, [$dfh] ];
     }
 
-    INFO("GET 403 $fullpath");
+    INFO("GET 403 23 $fullpath");
     return [ 403, [ $ct => $Trog::Vars::content_types{text} ], ["STAY OUT YOU RED MENACE"] ];
 }
 
@@ -104,7 +104,7 @@ sub _range ( $fullpath, $fh, $ranges, $sz, %headers ) {
     # Calculate the content-length up-front.  We have to fix unspecified lengths first, and reject bad requests.
     foreach my $range (@$ranges) {
         $range->[1] //= $sz - 1;
-        INFO("GET 416 $fullpath");
+        INFO("GET 416 31 $fullpath");
         return [ 416, [%headers], ["Requested range not satisfiable"] ] if $range->[0] > $sz || $range->[0] < 0 || $range->[1] < 0 || $range->[0] > $range->[1];
     }
     $headers{'Content-Length'} = List::Util::sum( map { my $arr = $_; $arr->[1] + 1, -$arr->[0] } @$ranges );
@@ -119,6 +119,7 @@ sub _range ( $fullpath, $fh, $ranges, $sz, %headers ) {
         $fc = '';
     }
 
+    #TODO log these requests with their length correctly
     return sub {
         my $responder = shift;
         my $writer;

+ 2 - 1
lib/Trog/Log.pm

@@ -36,7 +36,7 @@ sub log_message {
     # Rip apart the message.  If it's got any extended info, lets grab that too.
     my $msg = $params{message};
     my $message;
-    my ( $date, $uuid, $ip, $user, $method, $code, $route ) = $msg =~ m!^([\w|\-|:]+) \[INFO\]: RequestId ([\w|\-]+) From ([\w|\.|:]+) \|(\w+)\| (\w+) (\d+) (.+)!;
+    my ( $date, $uuid, $ip, $user, $method, $code, $bytes, $route ) = $msg =~ m!^([\w|\-|:]+) \[INFO\]: RequestId ([\w|\-]+) From ([\w|\.|:]+) \|(\w+)\| (\w+) (\d+) (\d+) (.+)!;
 
     # Otherwise, let's mark it down in the "messages" table.  This will be deferred until the final write.
     if ( !$date ) {
@@ -65,6 +65,7 @@ sub log_message {
     $ua      //= 'none';
     $urchin  //= {};
 
+    # TODO track bytes in the DB
     my $res = $self->{sth}->execute( $uuid, $epoch, $ip, $user, $method, $route, $referer, $ua, $code );
 
     # Dump in the accumulated messages

+ 2 - 1
lib/Trog/Log/Metrics.pm

@@ -89,7 +89,8 @@ sub _yeet ( $renderer, $error, %options ) {
     } or do {
         my $msg = $error;
         $msg .= " and subsequently during render of error template, $@" if $renderer;
-        INFO("$options{data}{method} 500 $options{data}{route}");
+        #XXX bytes is probably not correct here
+        INFO("$options{data}{method} 500 ".length($msg)." $options{data}{route}");
         FATAL($msg);
     };
     return $ret;

+ 6 - 1
lib/Trog/Renderer/Base.pm

@@ -326,7 +326,8 @@ sub _build_social_meta ( $query, $title ) {
     my $default_tags = $Theme::default_tags;
     $default_tags .= ',' . join( ',', @{ $query->{primary_post}->{tags} } ) if $default_tags && $query->{primary_post}->{tags};
 
-    my $meta_desc = $query->{primary_post}{data} // $Theme::description // "tCMS Site";
+    my $primary_data = ref $query->{primary_post}{data} eq 'ARRAY' ? $query->{primary_post}{data}[0] : $query->{primary_post}{data};
+    my $meta_desc = $primary_data // $Theme::description // "tCMS Site";
     $meta_desc = Trog::Utils::strip_and_trunc($meta_desc) || '';
 
     my $meta_tags = '';
@@ -872,6 +873,9 @@ sub post_save ($query) {
     my $acls = $query->{acls};
 
     $query->{tags} = Trog::Utils::coerce_array( $query->{tags} );
+    # Support data with multiple pages like presentations
+    $query->{data} = Trog::Utils::coerce_array( $query->{data} ) if $query->{data_is_array};
+    $query->{attachments} = Trog::Utils::coerce_array( $query->{attachments} );
 
     # Filter bits and bobs
     delete $query->{primary_post};
@@ -1261,6 +1265,7 @@ sub posts ( $query, $direct = 0 ) {
             months            => [ 0 .. 11 ],
             emoji_picker      => $picker,
             embed             => $query->{embed},
+            nochrome          => $query->{nochrome},
         },
         contenttype => 'text/html',
         component   => 1,

+ 10 - 2
lib/Trog/Routes/JSON.pm

@@ -13,6 +13,7 @@ use Mojo::File;
 use File::LibMagic;
 use Ref::Util qw{is_hashref};
 
+use Trog::Autoreload;
 use Trog::Log qw{WARN};
 use Trog::Config();
 
@@ -31,8 +32,9 @@ sub strip_and_trunc ($s) {
 
 # Instruct the parent to restart.  Normally this is HUP, but nginx-unit decides to be special.
 # Don't do anything if running NOHUP=1, which is useful when doing bulk operations
-sub restart_parent {
+sub restart_parent ( $parent=undef ) {
     return if $ENV{NOHUP};
+
     if ( $ENV{PSGI_ENGINE} && $ENV{PSGI_ENGINE} eq 'nginx-unit' ) {
         my $conf         = Trog::Config->get();
         my $nginx_socket = $conf->param('nginx-unit.socket');
@@ -41,8 +43,14 @@ sub restart_parent {
         WARN("could not reload application (got $res->{status} from nginx-unit)!") unless $res->{status} == 200;
         return 1;
     }
-    my $parent = getppid;
+    $parent //= getppid;
     kill 'HUP', $parent;
+
+    # Restart the monitor if it is lurking
+    my $monitor_pid = Trog::Autoreload::monitor_pid();
+    if ($monitor_pid) {
+        kill 'HUP', $monitor_pid;
+    }
 }
 
 sub uuid {

+ 4 - 1
lib/Trog/Vars.pm

@@ -2,6 +2,9 @@
     "name": "tCMS",
     "version": "1.0.0",
     "dependencies": {
-        "juice": "^8.0.0"
+        "juice": "^8.0.0",
+        "uglify": "^0.1.5",
+        "uglify-js": "^3.18.0",
+        "uglifyjs": "^2.4.11"
     }
 }

+ 8 - 5
schema/auth.schema

@@ -1,13 +1,16 @@
 [Unit]
-Description=tCMS
+Description=tCMS for __DOMAIN__
+Wants=multi-user.target
+After=multi-user.target
 
 [Install]
-WantedBy=default.target
+WantedBy=multi-user.target
 
 [Service]
-User=__DOMAIN__
-ExecStart=starman --listen __REPLACEME__/run/tcms.sock __REPLACEME__/www/server.psgi
-WorkingDirectory= __REPLACEME__/
+Type=forking
+PIDFile=__REPLACEME__/run/tcms.pid
+ExecStart=__REPLACEME__/tcms
+WorkingDirectory=__REPLACEME__/
 Restart=always
 OOMPolicy=stop
 ExecReload=kill -HUP $MAINPID

+ 18 - 2
t/Trog-SQLite.t

@@ -1,5 +1,21 @@
 #!/bin/bash
-[[ -e run/tcms.pid ]] && pkill -F run/tcms.pid
-sudo www/server.psgi --listen run/tcms.sock --workers 20 --group www-data --user $USER --daemonize --pid run/tcms.pid --chroot $(pwd)
+USERNAME=$(bin/tcms-hostname --user)
+echo "tCMS running as user $USERNAME"
+[[ -e run/tcms.pid ]] && sudo pkill -F run/tcms.pid
+sudo www/server.psgi --listen run/tcms.sock --workers 20 --user "$USERNAME" --daemonize --pid run/tcms.pid --chroot $(pwd)
+
+# Wait until the socket file is ready
+until [ -e run/tcms.sock ]
+do
+    echo "Waiting for sock to be ready..."
+    sleep 1
+done
+
+# Fix ownership of socket so nginx can see it
+sudo chown $USERNAME:www-data run/tcms.sock
 sudo chmod 0770 run/tcms.sock
+
 echo "tCMS running as PID "`cat run/tcms.pid`
+
+# Run reloader process
+bin/tcms_monitor

+ 4 - 0
ufw/git

@@ -0,0 +1,4 @@
+[git]
+title=Git SCM
+description=A distributed revision control system
+ports=53|22/tcp

+ 4 - 0
ufw/pdns_server

@@ -0,0 +1,4 @@
+[pdns-server]
+title=PowerDNS Server
+description=a caching nameserver
+ports=53

+ 13 - 5
ufw/setup-rules

@@ -10,7 +10,7 @@ my $DRY_RUN = $ARGV[0] ? 1 : 0;
 # Build rules, apply rules.
 
 # Enable every available service.
-# Don't use tCMS on hosts that do anything else with.
+# Don't use tCMS on hosts that do anything else.
 my $list = qx{ufw app list};
 my @apps = split(/\n/, $list);
 shift @apps;
@@ -18,6 +18,7 @@ shift @apps;
 
 # Sane defaults
 my @rules = (
+    [qw{reset}],
     [qw{enable}],
     [qw{default deny outgoing}],
     [qw{default deny incoming}],
@@ -26,16 +27,23 @@ my @rules = (
 # Allow, but rate limit
 foreach my $app (@apps) {
     push(@rules,
-        ["allow", $app],
-        ["limit", $app],
+        ["allow", "out", $app],
     );
+    # HTTP tends to request lots of stuff simultaneously
+    if ($app =~ m/^Nginx/i) {
+        push(@rules,
+            ["allow", "in", $app]
+        );
+    } else {
+        push(@rules,
+            ["limit", "in", $app],
+        );
+    }
 }
 
 @rules = map { unshift(@{$_}, '--dry-run'); $_ } @rules if $DRY_RUN;
 @rules = map { unshift(@{$_}, 'ufw'); $_ } @rules;
 
-print Dumper(\@rules);
-
 foreach my $rule (@rules) {
     system(@$rule);
 }

+ 109 - 0
www/.gitignore

@@ -1,3 +1,4 @@
+// Editor toggle
 function switchMenu(obj) {
     var el = document.getElementById(obj);
     if ( el === null ) {
@@ -11,6 +12,7 @@ function switchMenu(obj) {
     }
 }
 
+//Add items to the tags forms
 function add2tags(id) {
     var select = document.getElementById( id + '-tags');
     var input  = document.getElementById( id + '-customtag');
@@ -22,6 +24,7 @@ function add2tags(id) {
     input.value= '';
 }
 
+//Ditto for aliases
 function add2aliases(id) {
     var select = document.getElementById( id + '-alias');
     var input  = document.getElementById( id + '-customalias');
@@ -32,3 +35,109 @@ function add2aliases(id) {
     select.appendChild(newOption);
     input.value= '';
 }
+
+// Multi-page data functions
+function add2data(id, page) {
+    var select = document.getElementById( id + '-pages');
+    var input  = document.getElementById( id + '-' + page + '-page');
+    var thisPage = document.getElementById(id+'-'+page+'-page-option');
+    if (thisPage) {
+        thisPage.value = input.value;
+        return false;
+    }
+
+    var newOption = document.createElement('option');
+    newOption.value = input.value;
+    newOption.innerText = "Page " + page;
+    newOption.selected = true;
+    newOption.id = id+'-'+page+"-page-option";
+    select.appendChild(newOption);
+}
+
+function addPage(id) {
+    var pageContainer = document.getElementById( 'content-pages-' + id );
+    var lastPage = document.querySelector('#content-pages-'+id+' > textarea.data-page:nth-last-of-type(1)');
+    var curPage;
+    if (!lastPage) {
+        curPage = 0;
+    } else {
+        var matches = /^[a-f0-9\-]*-(\d+)-page$/.exec(lastPage.id);
+        // Go ahead and add this page in case we forgot.
+        add2data(id, matches[1]);
+        curPage = parseInt(matches[1]) + 1;
+    }
+    var newSpan = document.createTextNode('Page '+ curPage);
+    pageContainer.appendChild(newSpan);
+    var newButan = document.createElement('button');
+    newButan.type = "button";
+    newButan.className = "coolbutton emojiPicker";
+    newButan.innerText = "😎";
+    pageContainer.appendChild(newButan);
+    var newBr = document.createElement('br');
+    pageContainer.appendChild(newBr);
+    var newTextArea = document.createElement('textarea');
+    newTextArea.id = id + "-" + curPage + "-page";
+    newTextArea.className = 'cooltext data-page';
+    pageContainer.appendChild(newTextArea);
+}
+
+function addAllPages(id) {
+    var pages = document.querySelectorAll('#content-pages-'+id+' textarea');
+    for (page of pages) {
+        var matches = /^[a-f0-9\-]*-(\d+)-page$/.exec(page.id);
+        // Go ahead and add this page in case we forgot.
+        add2data(id, matches[1]);
+    }
+    return true;
+}
+
+// Post attachment forms
+function add2attachments(id, page) {
+    var select = document.getElementById( id + '-attachments');
+    var input  = document.getElementById( id + '-' + page + '-attachment');
+    var thisPage = document.getElementById(id+'-'+page+'-attachment-option');
+    if (thisPage) {
+        thisPage.value = input.value;
+        return false;
+    }
+
+    var newOption = document.createElement('option');
+    newOption.value = input.value;
+    newOption.innerText = "Attachment " + page;
+    newOption.selected = true;
+    newOption.id = id+'-'+page+"-attachment-option";
+    select.appendChild(newOption);
+}
+
+function addAttachment(id) {
+    var pageContainer = document.getElementById( 'content-attachments-' + id );
+    var lastPage = document.querySelector('#content-attachments-'+id+' > input.attachment:nth-last-of-type(1)');
+    var curPage;
+    if (!lastPage) {
+        curPage = 0;
+    } else {
+        var matches = /^[a-f0-9\-]*-(\d+)-attachment$/.exec(lastPage.id);
+        // Go ahead and add this page in case we forgot.
+        add2attachments(id, matches[1]);
+        curPage = parseInt(matches[1]) + 1;
+    }
+    var newSpan = document.createTextNode('Attachment '+ curPage);
+    pageContainer.appendChild(newSpan);
+    var newBr = document.createElement('br');
+    pageContainer.appendChild(newBr);
+    var newFilePicker = document.createElement('input');
+    newFilePicker.id = id + "-" + curPage + "-attachment";
+    newFilePicker.className = 'cooltext attachment';
+    newFilePicker.type = "file";
+    pageContainer.appendChild(newFilePicker);
+}
+
+function addAllAttachments(id) {
+    var pages = document.querySelectorAll('#content-attachments-'+id+' input');
+    for (page of pages) {
+        var matches = /^[a-f0-9\-]*-(\d+)-attachment$/.exec(page.id);
+        // Go ahead and add this page in case we forgot.
+        add2attachments(id, matches[1]);
+    }
+    return true;
+}

+ 0 - 5
www/server.psgi

@@ -6,12 +6,7 @@ use warnings;
 #Grab our custom routes
 use FindBin::libs;
 use TCMS;
-use Trog::Autoreload;
-
-our $MASTER_PID = $$;
 
 $ENV{PSGI_ENGINE} //= 'starman';
 
 our $app = \&TCMS::app;
-
-# TODO Fork off the supervisor process Trog::Autoreload

+ 9 - 0
www/styles/config.css

@@ -0,0 +1,9 @@
+Post Alias URLs:<br />
+<select class="cooltext" multiple name="aliases" id="<: $post.id :>-alias" >
+    : for $post.aliases -> $alias {
+        <option value="<: $alias :>" selected><: $alias :></option>
+    : }
+</select>
+Add Post Alias:<br />
+<input class="cooltext" type="text" id="<: $post.id :>-customalias" placeholder="/tickle" />
+<button style="float:right;" onclick="add2aliases('<: $post.id :>'); return false;" class="coolbutton" >Add</button>

+ 13 - 0
www/templates/html/components/attachments.tx

@@ -0,0 +1,13 @@
+Attachments: Add assets which may be referred to by ID in posts via $post.attachments[$attachment_number] in a <a href="https://metacpan.org/pod/Text::Xslate::Syntax::Kolon">Kolon Tag</a>.<br />
+<select class="cooltext" multiple name="attachments" id="<: $post.id :>-attachments" style="display:none;" >
+    : for $post.attachments -> $att {
+        <option value="<: $att :>" selected id="<: $post.id :>-<: $~att :>-attachment-option">Attachment <: $~att :></option>
+    : }
+</select>
+<div id="content-attachments-<: $post.id :>">
+: for $post.attachments -> $att {
+Attachment <: $~att :>:
+<br /><input type="file" class="cooltext attachment" id="<: $post.id :>-<: $~att :>-attachment" value="<: $att :>"></input>
+: }
+</div>
+<button style="float:right;" onclick="addAttachment('<: $post.id :>'); return false;" class="coolbutton" >New Attachment</button>

+ 0 - 10
www/templates/html/components/badrequest.tx

@@ -1,13 +1,3 @@
-Post Alias URLs:<br />
-<select class="cooltext" multiple name="aliases" id="<: $post.id :>-alias" >
-    : for $post.aliases -> $alias {
-        <option value="<: $alias :>" selected><: $alias :></option>
-    : }
-</select>
-Add Post Alias:<br />
-<input class="cooltext" type="text" id="<: $post.id :>-customalias" placeholder="/tickle" />
-<button style="float:right;" onclick="add2aliases('<: $post.id :>'); return false;" class="coolbutton" >Add</button>
-
 Content <button type="button" class="coolbutton emojiPicker">😎</button>:
 <br /><textarea class="cooltext" name="data" placeholder="Potzrebie"><: $post.data :></textarea>
 <input type="hidden" name="app" value="<: $app :>" />

+ 22 - 0
www/templates/html/components/form_multi.tx

@@ -0,0 +1,22 @@
+Pages:<br />
+<select class="cooltext" multiple name="data" id="<: $post.id :>-pages" style="display:none;" >
+    : for $post.data -> $page {
+        <option value="<: $page :>" selected id="<: $post.id :>-<: $~page :>-page-option">Page <: $~page :></option>
+    : }
+</select>
+<div id="content-pages-<: $post.id :>">
+: for $post.data -> $data {
+Page <: $~data :><button type="button" class="coolbutton emojiPicker">😎</button>:
+<br /><textarea class="cooltext data-page" id="<: $post.id :>-<: $~data :>-page" placeholder="Potzrebie"><: $data :></textarea>
+: }
+</div>
+<button style="float:right;" onclick="addPage('<: $post.id :>'); return false;" class="coolbutton" >New Page</button>
+
+<input type="hidden" name="app" value="<: $app :>" />
+<input type="hidden" name="to"  value="<: $route :>" />
+: if ( $post.id ) {
+<input type="hidden" name="id" value="<: $post.id :>" />
+: }
+<input type="hidden" name="form" value="<: $post.form :>"></input>
+<input type="hidden" name="data_is_array" value="1"></input>
+<input class="coolbutton" type="submit" onclick="addAllPages('<: $post.id :>') && addAllAttachments('<: $post.id :>')" value="Publish" text="Publish" />

+ 1 - 0
www/templates/html/components/forms/blog.tx

@@ -21,6 +21,7 @@
             : include "visibility.tx";
             : include "acls.tx";
             : include "tags.tx";
+            : include "aliases.tx";
             : include "form_common.tx";
         </form>
         : include "edit_foot.tx";

+ 1 - 0
www/templates/html/components/forms/dns.tx

@@ -55,6 +55,7 @@
         : include "visibility.tx";
         : include "acls.tx";
         : include "tags.tx";
+        : include "aliases.tx";
         : include "form_common.tx";
     </form>
     : include "edit_foot.tx";

+ 1 - 0
www/templates/html/components/forms/microblog.tx

@@ -38,6 +38,7 @@
             : include "visibility.tx";
             : include "acls.tx";
             : include "tags.tx";
+            : include "aliases.tx";
             : include "form_common.tx";
         </form>
         :include "edit_foot.tx"

+ 29 - 6
www/templates/html/components/forms/presentation.tx

@@ -3,36 +3,59 @@
         : include "post_title.tx";
         : include "post_tags.tx";
 
+        : if ( !$nochrome ) {
         : if ( !$post.video_href && !$post.is_image && !$post.is_video && !$post.is_profile && $post.preview ) {
             <img src="<: $post.preview :>" class="responsive-img" />
         : }
+        : }
 
-
+        :if ($nochrome) {
+        <style>
+            .postData {
+                margin-top:0px;
+            }
+            .responsive-text {
+                max-width:100vw !important;
+            }
+            .post {
+                height:100vh;
+            }
+        </style>
     	<div class="reveal postData responsive-text" id="postData-<: $post.id :>">
       		<div class="slides">
-      			<: for $post.data -> slide { :>
+      			<: for $post.data -> $slide { :>
       				<section>
                         <: render_it($slide) | mark_raw  :>
                     </section>
       			<: } :>
       		</div>
     	</div>
-    	<script src="scripts/reveal.js"></script>
+    	<script src="/scripts/reveal.js"></script>
     	<script>
-      		Reveal.initialize();
+      		Reveal.initialize({
+                hash:true,
+                embed:true
+            });
     	</script>
+        : } else {
+            <br />
+            <a href="/posts/<: $post.id :>?embed=1&nochrome=1">Click Here to view</a>
+        : }
     : }
 
-    : if ( $can_edit ) {
+    : if ( $can_edit && !$nochrome ) {
         <div class="postedit">
         : include "edit_head.tx";
         <form class="Submissions" action="/post/save" method="POST" enctype="multipart/form-data">
             Title *<br /><input required class="cooltext" type="text" name="title" placeholder="Iowa Man Destroys Moon" value="<: $post.title :>" />
             : include "preview.tx";
+            Video<br /><input class="cooltext" type="url" name="video_href" placeholder="https://youvimeo.tv/infomercial.mp4" value="<: $post.video_href :>" />
+            : include "attachments.tx";
             : include "visibility.tx";
             : include "acls.tx";
             : include "tags.tx";
-            : include "form_common.tx";
+            : include "aliases.tx"
+            : include "form_multi.tx";
         </form>
         : include "edit_foot.tx";
         </div>

+ 1 - 0
www/templates/html/components/forms/profile.tx

@@ -37,6 +37,7 @@
         : }
         <input type="hidden" name="callback" value="Trog::Routes::HTML::users" />
         : include "visibility.tx";
+        : include "aliases.tx";
         : include "form_common.tx";
     </form>
     : include "edit_foot.tx";

+ 1 - 0
www/templates/html/components/forms/series.tx

@@ -60,6 +60,7 @@
         : include "visibility.tx";
         : include "acls.tx";
         : include "tags.tx";
+        : include "aliases.tx";
         : include "form_common.tx";
     </form>
     : include "edit_foot.tx";

+ 2 - 1
www/templates/html/components/header.tx

@@ -1,5 +1,6 @@
-<span id="<: $post.id :>-tags-display">
+<span class="post-tags" id="<: $post.id :>-tags-display">
 : for $post.extra_tags -> $tag {
     <a href="/posts?tag=<: $tag :>" class="tag">🏷️ <: $tag :></a>
 : }
 </span><br />
+

+ 18 - 16
www/templates/html/components/post_title.tx

@@ -42,23 +42,25 @@ document.addEventListener("DOMContentLoaded", function(){
     hljs.highlightAll();
 });
 </script>
-<span class="title">
-: if ( $rss ) {
-<a title="RSS" class="rss" href="?format=rss"></a>
-<: $category :>:
-: }
-</span>
-: if ( $in_series && $rss ) {
-     <div class="subhead">
-     <: $subhead | mark_raw :>
-     </div>
-: }
+: if (!$nochrome) {
+    <span class="title">
+    : if ( $rss ) {
+    <a title="RSS" class="rss" href="?format=rss"></a>
+    <: $category :>:
+    : }
+    </span>
+    : if ( $in_series && $rss ) {
+         <div class="subhead">
+         <: $subhead | mark_raw :>
+         </div>
+    : }
 
-: if ( $header ) {
-    <: $header | mark_raw :>
-: }
-: if (!$style) {
-<hr class="divider" />
+    : if ( $header ) {
+        <: $header | mark_raw :>
+    : }
+    : if (!$style) {
+    <hr class="divider" />
+    : }
 : }
 : for $posts -> $post {
     : if ( !$post.form ) {

+ 4 - 4
www/templates/html/components/preview.tx

@@ -1,7 +1,7 @@
 <: $header | mark_raw :>
-:if ($embed_video) {
+:if ($embed_video || $nochrome) {
 <style>
-.divider, .blogtitles, #postData {
+.divider, .blogtitles, #postData, .post-tags {
     display:none;
 }
 video {
@@ -11,11 +11,11 @@ video {
 </style>
 : }
 
-: if (!$embed_video) {
+: if (!$embed_video && !$nochrome) {
 <div id="kontent" class="kontained">
 : }
     <: $content | mark_raw :>
-: if (!$embed_video) {
+: if (!$embed_video && !$nochrome) {
 </div>
 : }
 <: $footer | mark_raw :>

+ 0 - 0
www/templates/html/index.tx