George S. Baugh 5 tahun lalu
induk
melakukan
f87673f974
87 mengubah file dengan 2917 tambahan dan 0 penghapusan
  1. 436 0
      Indexer.pm
  2. 8 0
      Makefile
  3. 27 0
      Readme.md
  4. 3 0
      config/default.cfg
  5. 17 0
      lib/Trog/Config.pm
  6. 19 0
      lib/Trog/Data.pm
  7. 30 0
      lib/Trog/Data/DUMMY.pm
  8. 220 0
      lib/Trog/Routes/HTML.pm
  9. 30 0
      lib/Trog/Routes/JSON.pm
  10. 1 0
      microblog/.gitignore
  11. 1 0
      sys/admin/config/.gitignore
  12. 27 0
      sys/admin/config/main.json.example
  13. 14 0
      sys/admin/config/users.inc
  14. 12 0
      sys/admin/config/users.json.example
  15. 83 0
      sys/admin/index.php
  16. 100 0
      sys/blogroll.inc
  17. 1 0
      sys/fileshare/include/blacklist.json
  18. 12 0
      sys/fileshare/include/forbidden.inc
  19. 5 0
      sys/fileshare/include/notfound
  20. 20 0
      sys/fileshare/sanitize.inc
  21. 17 0
      sys/fileshare/showaudio.inc
  22. 36 0
      sys/fileshare/showcode.inc
  23. 15 0
      sys/fileshare/showdoc.inc
  24. 143 0
      sys/fileshare/showfiles.inc
  25. 15 0
      sys/fileshare/showimg.inc
  26. 23 0
      sys/fileshare/showpost.inc
  27. 19 0
      sys/fileshare/showvideo.inc
  28. 161 0
      sys/microblog.inc
  29. 71 0
      sys/rss/blog.php
  30. 78 0
      sys/rss/microblog.php
  31. TEMPAT SAMPAH
      www/img/avatar/humm.gif
  32. TEMPAT SAMPAH
      www/img/avatar/hydra-lel.png
  33. TEMPAT SAMPAH
      www/img/avatar/naptime.png
  34. TEMPAT SAMPAH
      www/img/icon/rss.png
  35. 112 0
      www/img/icon/tCMS.svg
  36. TEMPAT SAMPAH
      www/img/mime/denied.gif
  37. TEMPAT SAMPAH
      www/img/mime/missing.gif
  38. TEMPAT SAMPAH
      www/img/mime/tsarchive.gif
  39. TEMPAT SAMPAH
      www/img/mime/tsaudio.gif
  40. TEMPAT SAMPAH
      www/img/mime/tscode.gif
  41. TEMPAT SAMPAH
      www/img/mime/tsdoc.png
  42. TEMPAT SAMPAH
      www/img/mime/tsdownload.gif
  43. TEMPAT SAMPAH
      www/img/mime/tsfile.gif
  44. TEMPAT SAMPAH
      www/img/mime/tsfolder-up.gif
  45. TEMPAT SAMPAH
      www/img/mime/tsfolder.gif
  46. TEMPAT SAMPAH
      www/img/mime/tsimage.gif
  47. TEMPAT SAMPAH
      www/img/mime/tsmodel.gif
  48. TEMPAT SAMPAH
      www/img/mime/tsmovie.gif
  49. TEMPAT SAMPAH
      www/img/mime/tsprop.png
  50. TEMPAT SAMPAH
      www/img/mime/tsschematic.gif
  51. TEMPAT SAMPAH
      www/img/mime/tssoftware.gif
  52. TEMPAT SAMPAH
      www/img/mime/tssticky.gif
  53. TEMPAT SAMPAH
      www/img/sys/testpattern.jpg
  54. 0 0
      www/scripts/main.js
  55. 140 0
      www/server.psgi
  56. 6 0
      www/styles/avatars.css
  57. 63 0
      www/styles/login.css
  58. 39 0
      www/styles/notconfigured.css
  59. 34 0
      www/styles/print.css
  60. 290 0
      www/styles/screen.css
  61. 96 0
      www/styles/structure.css
  62. 41 0
      www/templates/config.tx
  63. 1 0
      www/templates/default.tx
  64. 1 0
      www/templates/footbar.tx
  65. 2 0
      www/templates/footer.tx
  66. 20 0
      www/templates/header.tx
  67. 35 0
      www/templates/index.tx
  68. 1 0
      www/templates/leftbar.tx
  69. 57 0
      www/templates/login.tx
  70. 0 0
      www/templates/midtitle.tx
  71. 21 0
      www/templates/notconfigured.tx
  72. 18 0
      www/templates/post.tx
  73. 4 0
      www/templates/posts.tx
  74. 1 0
      www/templates/rightbar.tx
  75. 1 0
      www/templates/title.tx
  76. TEMPAT SAMPAH
      www/themes/teodesian.net/img/icon/dave.png
  77. TEMPAT SAMPAH
      www/themes/teodesian.net/img/misc/linode.png
  78. TEMPAT SAMPAH
      www/themes/teodesian.net/img/sys/desian.png
  79. TEMPAT SAMPAH
      www/themes/teodesian.net/img/sys/logo.png
  80. TEMPAT SAMPAH
      www/themes/teodesian.net/img/sys/tsn.png
  81. 25 0
      www/themes/teodesian.net/routes.pm
  82. 66 0
      www/themes/teodesian.net/styles/screen.css
  83. 8 0
      www/themes/teodesian.net/styles/structure.css
  84. 21 0
      www/themes/teodesian.net/templates/leftbar.tx
  85. 168 0
      www/themes/teodesian.net/templates/links.tx
  86. 1 0
      www/themes/teodesian.net/templates/midtitle.tx
  87. 1 0
      www/themes/teodesian.net/templates/title.tx

+ 436 - 0
Indexer.pm

@@ -0,0 +1,436 @@
+# ABSTRACT: Define what data is to be uploaded to elasticsearch, and handle it's uploading
+# PODNAME: App::Prove::Elasticsearch::Indexer
+
+package App::Prove::Elasticsearch::Indexer;
+
+use strict;
+use warnings;
+
+use App::Prove::Elasticsearch::Utils();
+
+use Search::Elasticsearch();
+use JSON::MaybeXS();
+use File::Temp();
+use File::Slurper();
+use List::Util 1.33;
+
+=head1 SYNOPSIS
+
+    App::Prove::Elasticsearch::Indexer::check_index({ 'server.host' => 'zippy.test', 'server.port' => 9600 });
+
+=head1 VARIABLES
+
+=head2 index (STRING)
+
+The name of the elasticsearch index used.
+If you are subclassing this, be aware that the Searcher plugin will rely on this.
+
+=cut
+
+our $index = 'testsuite';
+
+sub index {
+    return $index;
+}
+
+=head2 max_query_size
+
+Number of items returned by queries.
+Defaults to 1000.
+
+=cut
+
+our $max_query_size = 1000;
+our $e;
+our $bulk_helper;
+our $idx;
+our %stashed;
+
+=head1 SUBROUTINES
+
+=head2 check_index
+
+Returns 1 if the index needed to be created, 0 if it's already OK.
+Dies if the server cannot be reached, or the index creation fails.
+
+=cut
+
+sub check_index {
+    my $conf = shift;
+
+    my $port = $conf->{'server.port'} ? ':' . $conf->{'server.port'} : '';
+    die "server must be specified" unless $conf->{'server.host'};
+    die("port must be specified")  unless $port;
+    my $serveraddress = "$conf->{'server.host'}$port";
+    $e //= Search::Elasticsearch->new( nodes => $serveraddress, );
+
+    #XXX for debugging
+    #$e->indices->delete( index => $index );
+
+    if ( !$e->indices->exists( index => $index ) ) {
+        $e->indices->create(
+            index => $index,
+            body  => {
+                index => {
+                    similarity => {
+                        default => {
+                            type => "classic"
+                        }
+                    }
+                },
+                analysis => {
+                    analyzer => {
+                        default => {
+                            type      => "custom",
+                            tokenizer => "whitespace",
+                            filter    => [
+                                'lowercase', 'std_english_stop', 'custom_stop'
+                            ]
+                        }
+                    },
+                    filter => {
+                        std_english_stop => {
+                            type      => "stop",
+                            stopwords => "_english_"
+                        },
+                        custom_stop => {
+                            type      => "stop",
+                            stopwords => [ "test", "ok", "not" ]
+                        }
+                    }
+                },
+                mappings => {
+                    testsuite => {
+                        properties => {
+                            id       => { type => "integer" },
+                            elapsed  => { type => "integer" },
+                            occurred => {
+                                type   => "date",
+                                format => "yyyy-MM-dd HH:mm:ss"
+                            },
+                            executor => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            status => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            version => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            test_version => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            platform => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            path => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            defect => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            steps_planned => { type => "integer" },
+                            body          => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                            },
+                            name => {
+                                type        => "text",
+                                analyzer    => "default",
+                                fielddata   => "true",
+                                term_vector => "yes",
+                                similarity  => "classic",
+                                fields      => {
+                                    keyword => { type => "keyword" }
+                                }
+                            },
+                            steps => {
+                                properties => {
+                                    number  => { type => "integer" },
+                                    text    => { type => "text" },
+                                    status  => { type => "text" },
+                                    elapsed => { type => "integer" },
+                                }
+                            },
+                        }
+                    }
+                }
+            }
+        );
+        return 1;
+    }
+    return 0;
+}
+
+=head2 index_results
+
+Index a test result (see L<App::Prove::Elasticsearch::Parser> for the input).
+
+=cut
+
+sub index_results {
+    my ($result) = @_;
+
+    die("check_index must be run first") unless $e;
+
+    $idx //= App::Prove::Elasticsearch::Utils::get_last_index( $e, $index );
+    $idx++;
+
+    eval {
+        $e->index(
+            index => $index,
+            id    => $idx,
+            type  => $index,
+            body  => $result,
+        );
+    };
+    if ($@) {
+        if ( ( ref $@ ) eq "Search::Elasticsearch::Error::NoNodes" ) {
+            print "Failed to index due to no nodes online - continuing: $@\n";
+            _stash_result_for_retry($result);
+            return;
+        }
+    }
+
+    my $doc_exists =
+      $e->exists( index => $index, type => 'testsuite', id => $idx );
+    if ( !defined($doc_exists) || !int($doc_exists) ) {
+        die
+"Failed to Index $result->{'name'}, could find no record with ID $idx\n";
+    }
+    print "Successfully Indexed test: $result->{'name'} with result ID $idx\n";
+
+    #Now that we've proven the server is up, let's re-upload if we need to.
+    _resubmit_stashed_documents();
+}
+
+sub _stash_result_for_retry {
+    my ($result) = @_;
+    my $encoder  = JSON::MaybeXS->new( utf8 => 1 );
+    my $dump     = $encoder->encode_json($result);
+    my ( $fh, $filename ) = File::Temp::tempfile();
+    print $fh $dump;
+
+#Keep a hold of the File::Temp object so that when our program goes out of scope everything is cleaned up.
+    $stashed{$filename} = $fh;
+}
+
+# Let's give it the "college try TM" with a bulk dump, and give up if that second try fails
+sub _resubmit_stashed_documents {
+    my @files = keys(%stashed);
+    return unless @files;
+
+    my $encoder   = JSON::MaybeXS->new( utf8 => 1 );
+    my @bulk_load = map {
+        my $out = File::Slurper::read_text($_);
+        $encoder->decode_json($out);
+        close $stashed{$_};
+        delete $stashed{$_};
+    } @files;
+    return bulk_index_results(@bulk_load);
+}
+
+=head2 bulk_index_results(@results)
+
+Helper method for migration scripts.
+Uploads an array of results in bulk such as would be fed to index_results.
+
+It is up to the caller to chunk inputs as is appropriate for your installation.
+
+=cut
+
+sub bulk_index_results {
+    my @results = @_;
+    $bulk_helper //= $e->bulk_helper(
+        index => $index,
+        type  => $index,
+    );
+
+    $idx //= App::Prove::Elasticsearch::Utils::get_last_index( $e, $index );
+
+    $bulk_helper->index( map { $idx++; { id => $idx, source => $_ } }
+          @results );
+    $bulk_helper->flush();
+}
+
+=head2 associate_case_with_result(%config)
+
+Associate an indexed result with a tracked defect.
+
+Requires configuration to be inside of ENV vars already.
+
+Arguments Hash:
+
+=over 4
+
+=item B<case STRING>     - case to associate defect to
+
+=item B<defects ARRAY>   - defects to associate with case
+
+=item B<platforms ARRAY> - filter out any results not having these platforms
+
+=item B<versions ARRAY>  - filter out any results not having these versions
+
+=back
+
+=cut
+
+sub associate_case_with_result {
+    my %opts = @_;
+
+    die("check_index must be run first") unless $e;
+
+    my %q = (
+        index => $index,
+        body  => {
+            query => {
+                bool => {
+                    must => [
+                        {
+                            match => {
+                                name => $opts{case},
+                            }
+                        },
+                    ],
+                },
+            },
+        },
+    );
+
+    #It's normal to have multiple platforms in a document.
+    foreach my $plat ( @{ $opts{platforms} } ) {
+        push(
+            @{ $q{body}{query}{bool}{must} },
+            { match => { platform => $plat } }
+        );
+    }
+
+    #It's NOT normal to have multiple versions in a document.
+    foreach my $version ( @{ $opts{versions} } ) {
+        push(
+            @{ $q{body}{query}{bool}{should} },
+            { match => { version => $version } }
+        );
+    }
+
+    #Paginate the query, TODO short-circuit when we stop getting results?
+    my $hits = App::Prove::Elasticsearch::Utils::do_paginated_query( $e,
+        $max_query_size, %q );
+    return 0 unless scalar(@$hits);
+
+    #Now, update w/ the defect.
+    my $failures = 0;
+    my $attempts = 0;
+    foreach my $hit (@$hits) {
+        $hit->{_source}->{platform} = [ $hit->{_source}->{platform} ]
+          if ref( $hit->{_source}->{platform} ) ne 'ARRAY';
+        next
+          if ( scalar( @{ $opts{versions} } ) && !$hit->{_source}->{version} );
+        next
+          unless List::Util::any { $hit->{_source}->{version} eq $_ }
+        @{ $opts{versions} };
+        next
+          if ( scalar( @{ $opts{platforms} } )
+            && !$hit->{_source}->{platform} );
+        next unless List::Util::all {
+            my $p = $_;
+            grep { $_ eq $p } @{ $hit->{_source}->{platform} }
+        }
+        @{ $opts{platforms} };
+        next unless $hit->{_source}->{name} eq $opts{case};
+
+        $attempts++;
+
+        #Merge the existing defects with the ones we are adding in
+        $hit->{defect} //= [];
+        my @df_merged =
+          List::Util::uniq( ( @{ $hit->{defect} }, @{ $opts{defects} } ) );
+
+        my %update = (
+            index => $index,
+            id    => $hit->{_id},
+            type  => 'result',
+            body  => {
+                doc => {
+                    defect => \@df_merged,
+                },
+            }
+        );
+        $update{body}{doc}{status} = $opts{status} if $opts{status};
+
+        my $res = $e->update(%update);
+
+        print "Associated cases to document $hit->{_id}\n"
+          if $res->{result} eq 'updated';
+        if ( !grep { $res->{result} eq $_ } qw{updated noop} ) {
+            print
+"Something went wrong associating cases to document $hit->{_id}!\n$res->{result}\n";
+            $failures++;
+        }
+    }
+
+    print "No cases matching your query could be found.  No action was taken.\n"
+      unless $attempts;
+
+    return $failures;
+}
+
+1;
+
+__END__
+
+=head1 SPECIAL THANKS
+
+Thanks to cPanel Inc, for graciously funding the creation of this module.

+ 8 - 0
Makefile

@@ -0,0 +1,8 @@
+.PHONY: test
+test:
+	prove t/*.t
+
+.PHONY: depend
+depend:
+	sudo apt install cpanminus starman  libcal-dav-perl libtext-xslate-perl libserver-starter-perl liburl-encode-perl libplack-perl libcal-dav-perl libconfig-tiny-perl libdatetime-format-http-perl libjson-maybexs-perl libuuid-tiny-perl libcapture-tiny-perl libconfig-simple-perl
+	sudo cpanm Mojo::File Date::Format

+ 27 - 0
Readme.md

@@ -0,0 +1,27 @@
+tCMeS
+=====
+
+It's basically tCMS, with elasticsearch as the storage backend
+
+Oh it's also a Perl PSGI app with a flippin' api now too :P
+
+Ideas:
+======
+Put *all* posts in elasticsearch, just filter by type and have a micro, blog, image (insta), video, podcat and wiki view with static renders
+
+Search bar that isn't SHIT
+
+*domain* picker at top -- manage all your web properties from one place
+
+login and registration (forces email for a domain to allow posting on said domain)
+User data *also* stored in ES -- it's their profile page!
+
+Error and Access logs immediately dumped into ES for EZ viewing in grafana
+
+Automatic analytics!
+
+Builtin paywall -- add in LDAP users not on primary domain, give differing privs
+Have all content able to assign to paywall packages
+
+One click share to social via oauth
+Mailing list blasts for paywall content

+ 3 - 0
config/default.cfg

@@ -0,0 +1,3 @@
+[general]
+    data_model=DUMMY
+    title=tCMS

+ 17 - 0
lib/Trog/Config.pm

@@ -0,0 +1,17 @@
+package Trog::Config;
+
+use strict;
+use warnings;
+
+use Config::Simple;
+
+sub get {
+    my $cf = {};
+    my $home_cfg = "$ENV{HOME}/.tcms/main.cfg"; #XXX probably should pass this in and sanitize ENV
+    Config::Simple->import_from($home_cfg, $cf) if -f $home_cfg;
+    return $cf if %$cf;
+    Config::Simple->import_from('config/default.cfg', $cf);
+    return $cf;
+}
+
+1;

+ 19 - 0
lib/Trog/Data.pm

@@ -0,0 +1,19 @@
+package Trog::Data;
+
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+#It's just a factory
+
+sub new($class,$config) {
+    my $module = "Trog::Data::$config->{'general.data_model'}";
+    my $req = $module;
+    $req =~ s/::/\//g;
+    require "$req.pm";
+    return $module->new($config);
+}
+
+1;

+ 30 - 0
lib/Trog/Data/DUMMY.pm

@@ -0,0 +1,30 @@
+package Trog::Data::DUMMY;
+
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+sub new ($class, $config) {
+    return bless($config,__PACKAGE__);
+}
+
+# These have to be sorted as requested by the client
+sub get ($self, %request) {
+    return [{ data => "<hr><h3 class='blogtitles'><a href='/'>Example Post</a></h3>Here, caveman", id => 666 }]
+}
+
+sub add ($self, @posts) {
+    return 1;
+}
+
+sub update($self, @posts) {
+    return 1;
+}
+
+sub delete($self, @ids) {
+    return 1;
+}
+
+1;

+ 220 - 0
lib/Trog/Routes/HTML.pm

@@ -0,0 +1,220 @@
+package Trog::Routes::HTML;
+
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+use Trog::Config;
+my $conf = Trog::Config::get();
+my $template_dir = 'www/templates';
+my $theme_dir;
+$theme_dir = "themes/$conf->{'general.theme'}" if $conf->{'general.theme'} && -d "www/themes/$conf->{'general.theme'}";
+
+use lib 'www';
+
+# TODO Things which should be themable
+our $landing_page = 'default.tx';
+our $htmltitle     = 'title.tx';
+our $midtitle      = 'midtitle.tx';
+our $rightbar     = 'rightbar.tx';
+our $leftbar      = 'leftbar.tx';
+our $footbar      = 'footbar.tx';
+
+our %routes = (
+    '/' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::index,
+    },
+    '/setup' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::setup,
+    },
+    '/login' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::login,
+    },
+    '/auth' => {
+        method   => 'POST',
+        callback => \&Trog::Routes::HTML::login,
+    },
+    '/config' => {
+        method   => 'GET',
+        auth     => 1,
+        callback => \&Trog::Routes::HTML::config,
+    },
+    '/config/save' => {
+        method   => 'POST',
+        auth     => 1,
+        callback => \&Trog::Routes::HTML::config,
+    },
+    '/post' => {
+        method   => 'GET',
+        auth     => 1,
+        callback => \&Trog::Routes::HTML::post,
+    },
+    '/post/save' => {
+        method   => 'POST',
+        auth     => 1,
+        callback => \&Trog::Routes::HTML::post,
+    },
+    '/posts' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::posts,
+    },
+    '/files' => {
+        method   => 'GET',
+        callback => \&Trog::Routes::HTML::files
+    },
+);
+
+# Build aliases for /post with extra data
+my @post_aliases = qw{news blog wiki video audio about};
+@routes{map { "/$_" } @post_aliases} = map { my %copy = %{$routes{'/posts'}}; $copy{data} = { tag => [$_] }; \%copy } @post_aliases;
+
+# Grab theme routes
+if ($theme_dir) {
+    my $theme_mod = "$theme_dir/routes.pm";
+    if (-f $theme_mod ) {
+        require $theme_mod;
+        @routes{keys(%Theme::routes)} = values(%Theme::routes);
+    }
+}
+
+sub index ($query, $input, $render_cb, $content = '', $i_styles = []) {
+    $input->{theme_dir}  = $theme_dir || '';
+
+    my $processor = Text::Xslate->new(
+        path   => $template_dir,
+    );
+
+    my $t_processor;
+    $t_processor = Text::Xslate->new(
+        path =>  "www/$theme_dir/templates",
+    ) if $theme_dir;
+
+    $content ||= _pick_processor($rightbar,$processor,$t_processor)->render($landing_page,$input);
+
+    my @styles = ('/styles/avatars.css'); #TODO generate file for users
+    if ($theme_dir) {
+        unshift(@styles, _themed_style("screen.css"))    if -f 'www/'._themed_style("screen.css");
+        unshift(@styles, _themed_style("structure.css")) if -f 'www/'._themed_style("structure.css");
+    }
+    push( @styles, @$i_styles);
+
+    #TODO allow theming of print css
+
+    return $render_cb->('index.tx',{
+        user        => $query->{user},
+        theme_dir   => $theme_dir,
+        content     => $content,
+        title       => $conf->{'general.title'},
+        htmltitle   => _pick_processor("templates/$htmltitle" ,$processor,$t_processor)->render($htmltitle,$input),
+        midtitle    => _pick_processor("templates/$midtitle" ,$processor,$t_processor)->render($midtitle,$input),
+        rightbar    => _pick_processor("templates/$rightbar" ,$processor,$t_processor)->render($rightbar,$input),
+        leftbar     => _pick_processor("templates/$leftbar"  ,$processor,$t_processor)->render($leftbar,$input),
+        footbar     => _pick_processor("templates/$footbar"  ,$processor,$t_processor)->render($footbar,$input),
+        stylesheets => \@styles,
+    });
+}
+
+sub setup ($query, $input, $render_cb) {
+    return $render_cb->('notconfigured.tx', {
+        title => 'tCMS Requires Setup to Continue...',
+        stylesheets => _build_themed_styles('notconfigured.css'),
+    });
+}
+
+sub login ($query, $input, $render_cb) {
+    # TODO actually do login processing
+
+    $query->{failed} //= -1;
+    return $render_cb->('login.tx', {
+        title         => 'tCMS 2 ~ Login',
+        to            => $query->{to} || '/config',
+        login_failure => int( $query->{failed} ),
+        login_message => int( $query->{failed} ) < 1 ? "Login Successful, Redirecting..." : "Login Failed.",
+        stylesheets   => _build_themed_styles('login.css'),
+    });
+}
+
+sub config ($query, $input, $render_cb) {
+    return $render_cb->('config.tx', {
+        title => 'Configure tCMS',
+        stylesheets => _build_themed_styles('config.css'),
+    });
+}
+
+sub config_save ($query, $input, $render_cb) {
+    return config($query, $input, $render_cb);
+}
+
+sub post ($query, $input, $render_cb) {
+    return $render_cb->('post.tx', {
+        title => 'New Post',
+        stylesheets => _build_themed_styles('post.css'),
+    });
+}
+
+sub post_save ($query, $input, $render_cb) {
+    return post($query, $input, $render_cb);
+}
+
+sub posts ($query, $input, $render_cb) {
+    my $tags = _coerce_array($query->{tag});
+
+    require Trog::Data;
+    my $data = Trog::Data->new($conf);
+
+    my $processor ||= Text::Xslate->new(
+        path   => _dir_for_resource('posts.tx'),
+    );
+
+    my $styles = _build_themed_styles('posts.css');
+
+    my $content = $processor->render('posts.tx', {
+        title => "Posts tagged @$tags",
+        date  => 'TODO',
+        posts => $data->get(
+            tags  => $tags,
+            like => $query->{like},
+        ),
+    });
+    return Trog::Routes::HTML::index($query, $input, $render_cb, $content, $styles);
+}
+
+sub files ($query, $input, $render_cb) {
+    return $render_cb->('fileman.tx', {
+        title => 'tCMS File Browser',
+        stylesheets => _build_themed_styles('fileman.css'),
+    });
+}
+
+# Deal with Params which may or may not be arrays
+sub _coerce_array ($param) {
+    my $p = $param || [];
+    $p = [$param] if $param && (ref $param ne 'ARRAY');
+    return $p;
+}
+
+sub _build_themed_styles ($style) {
+    my @styles = ("/styles/$style");
+    my $ts = _themed_style($style);
+    push(@styles, $ts) if $theme_dir && -f $ts;
+    return \@styles;
+}
+
+sub _pick_processor($file, $normal, $themed) {
+    return _dir_for_resource($file) eq $template_dir ? $normal : $themed;
+}
+
+# Pick appropriate dir based on whether theme override exists
+sub _dir_for_resource ($resource) {
+    return $theme_dir && -f "www/$theme_dir/$resource" ? $theme_dir : $template_dir;
+}
+sub _themed_style ($resource) {
+    return _dir_for_resource("styles/$resource")."/styles/$resource";
+}
+
+1;

+ 30 - 0
lib/Trog/Routes/JSON.pm

@@ -0,0 +1,30 @@
+package Trog::Routes::JSON;
+
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+use JSON::MaybeXS();
+
+our %routes = (
+    '/api/catalog' => {
+        method     => 'GET',
+        callback   => \&catalog,
+        parameters => [],
+    },
+);
+
+my $contenttype = "Content-type:application/json;";
+
+sub catalog ($query, $input, $=) {
+    my $enc = JSON::MaybeXS->new( utf8 => 1 );
+    my %rcopy = %{\%routes};
+    foreach my $r (keys(%rcopy)) {
+        delete $rcopy{$r}{callback}
+    }
+    return [200,[$contenttype],[$enc->encode(\%rcopy)]];
+}
+
+1;

+ 1 - 0
microblog/.gitignore

@@ -0,0 +1 @@
+[^.]*

+ 1 - 0
sys/admin/config/.gitignore

@@ -0,0 +1 @@
+*.json

+ 27 - 0
sys/admin/config/main.json.example

@@ -0,0 +1,27 @@
+{
+    "toptitle" : "templates/default/title.inc",
+    "leftbar" : "templates/default/leftbar.inc",
+    "rightbar" : "templates/default/rightbar.inc",
+    "footbar" : "templates/default/footbar.inc",
+    "about" : "templates/default/about.inc",
+    "home" : "sys/blogroll.inc",
+    "fileshare" : "sys/fileshare/showfiles.inc",
+    "microblog" : "sys/microblog.inc",
+    "blog" : "sys/blogroll.inc",
+    "postloader" : "sys/fileshare/showpost.inc",
+    "codeloader" : "sys/fileshare/showcode.inc",
+    "audioloader" : "sys/fileshare/showaudio.inc",
+    "videoloader" : "sys/fileshare/showvideo.inc",
+    "imgloader" : "sys/fileshare/showimg.inc",
+    "docloader" : "sys/fileshare/showdoc.inc",
+    "blogdir" : "blog/",
+    "microblogdir" : "microblog/",
+    "filesharedir" : "fileshare",
+    "rssdir" : "sys/rss/",
+    "icondir" : "img/mime/",
+    "basedir" : "",
+    "htmltitle" : "Unconfigured tCMS Website",
+    "blogtitle" : "Blog",
+    "microblogtitle" : "Linklog",
+    "timezone" : "America/Chicago"
+}

+ 14 - 0
sys/admin/config/users.inc

@@ -0,0 +1,14 @@
+<?php
+$tcmsUsers = json_decode(file_get_contents('config/users.json'),true);
+if (!empty($_SERVER['REMOTE_USER']) && !empty($tcmsUsers)) {
+    $poster = "admin";
+    foreach (array_keys($tcmsUsers) as $user) {
+        if( $tcmsUsers[$user]['remoteUser'] == $_SERVER['REMOTE_USER']) {
+            $poster = $user;
+            break;
+        }   
+    }   
+} else {
+    $poster = "Nobody";
+}
+?>

+ 12 - 0
sys/admin/config/users.json.example

@@ -0,0 +1,12 @@
+{
+  "bambam" : {
+    "fullName" : "The Administrator",
+    "email" : "admin@example.com",
+    "remoteUser: "barney"
+  },
+  "wilma" : {
+    "fullName" : "Mr. Magoo",
+    "email" : "dev@null.io",
+    "remoteUser: "fred"
+  }
+}

+ 83 - 0
sys/admin/index.php

@@ -0,0 +1,83 @@
+<?php
+
+    // Setup includes to work right. Much of this is duped in Config.inc, but gotta get this info to include it, so..
+    $user_info = posix_getpwuid(posix_geteuid());
+    $dir = ( $user_info['dir'] ? $user_info['dir'] : '/var/www/' );
+    $basedir = ( file_exists( $dir . "/.tCMS_basedir") ? file_get_contents("$dir/.tCMS_basedir") : "$dir/.tCMS" );
+    set_include_path(get_include_path() . PATH_SEPARATOR . "$basedir/lib");
+    require_once "tCMS/Config.inc";
+
+    // Get the config, set the theme (also set the basedir so we don't have to fetch it again).
+    $conf_obj = new Config;
+    $conf_obj->set_base_dir($basedir);
+    $config = $conf_obj->get();
+    $theme = ( !array_key_exists( 'theme', $config ) || empty($config['theme']) ? 'default' : $config['theme'] );
+    $themedir = "$basedir/templates/$theme";
+
+    // Begin dispatch
+    $args = ( $_SERVER['REQUEST_METHOD'] == 'POST' ? $_POST : $_GET );
+    if( !empty($args['app']) && $args['app'] == 'login' ) {
+        include "$themedir/admin/login.inc";
+        die();
+    } elseif( !empty($args['app']) && $args['app'] == 'logout' ) {
+        include "$themedir/admin/logout.inc";
+        die();
+    } else {
+        require_once "tCMS/Auth.inc";
+        $auth = new Auth;
+        $auth->ensure_auth();
+    }
+    if( empty($args['app']) || $args['app'] == 'config' ) {
+        $kontent = "$themedir/admin/settings.inc";
+    } elseif ($args['app'] == 'blog') {
+        if(!empty($args['get_fragment'])) {
+            # Need to sanitize
+            $path = realpath("$basedir/blog/".$args['get_fragment']);
+            if(strpos($path, "$basedir/blog") !== 0 ) die("Forbidden: Tried to load $path, but $basedir/blog is not the start of the real path.");
+            die(file_get_contents("$basedir/blog/".$args['get_fragment']));
+        }
+        $kontent = "$themedir/admin/bengine.inc";
+    } elseif ($args['app'] == 'microblog') {
+        $kontent = "$themedir/admin/mbengine.inc";
+    } elseif ($args['app'] == 'users' ) {
+        $kontent = "$themedir/admin/users.inc";
+    } else {
+        $kontent = "$themedir/admin/settings.inc";
+    }
+?>
+<!doctype html>
+<html dir="ltr" lang="en-US">
+ <head>
+  <meta charset="utf-8" />
+  <meta name="description" content="tCMS Control Panel"/>
+  <meta name="viewport" content="width=device-width">
+  <?php
+    $links  = '<link rel="stylesheet" type="text/css" href="../../themed/' . $theme . '/css/structure.css" />';
+    $links .= '<link rel="stylesheet" type="text/css" href="../../themed/' . $theme . '/css/screen.css" media="screen" />';
+    $links .= '<link rel="stylesheet" type="text/css" href="../../themed/' . $theme . '/css/print.css" media="print" />';
+    $links .= '<link rel="icon" type="image/vnd.microsoft.icon" href="../../themed/' . $theme . '/img/icon/favicon.ico" />';
+    echo $links;
+
+    // TODO inject avatars these via style tags based on config
+  ?>
+  <title>tCMS Admin</title>
+  <?php
+  ?>
+ </head>
+ <body>
+  <div id="topkek" style="text-align: center; vertical-align: middle;">
+   <button title="Menu" id="clickme">&#9776;</button>
+   <span id="configbar">
+    <a class="topbar" title="Edit Various Settings" href="index.php?app=config">Settings</a>
+    <a class="topbar" title="Blog Writer" href="index.php?app=blog">Blog Writer</a>
+    <a class="topbar" title="Pop off about Stuff" href="index.php?app=microblog">MicroBlogger</a>
+    <a class="topbar" title="Logout" href="index.php?app=logout">Logout</a>
+   </span>
+  </div>
+  <div id="kontent" style="display: block;">
+   <?php
+        include $kontent;
+   ?>
+  </div>
+ </body>
+</html>

+ 100 - 0
sys/blogroll.inc

@@ -0,0 +1,100 @@
+<p class="title">
+ <a title="RSS" href="sys/rss/blog.php" class="rss"></a>
+ <?php
+  echo $config['blogtitle'];
+ ?>
+ <hr />
+</p>
+<?php
+
+if (!empty($_GET['post'])) {
+    $post=urldecode($_GET['post']);
+    $statz = stat($post);
+    $uid = $statz['uid'];
+    $udata = posix_getpwuid($uid);
+    $user = $udata['name'];
+
+    $date =  date("F d Y H:i:s", filemtime($post));
+
+    if (stristr($post,'.') != ".post") {
+        $title = basename($post);
+    } else {
+        $title = substr(strstr(basename($post),'-'),1,-5);
+    }
+
+    echo "<h3 class=\"blogtitles\"><a title=permalink href=\"index.php?nav=3&post=".urlencode($post)."\">$title</a></h3>\n";
+    echo "<em class=\"blogdetail\">Last modified on $date UTC by $user</em><hr />\n\n";
+    include "$post";
+    echo "\n<hr /><a style=\"textalign: center;\" href=\"".$_SERVER["PHP_SELF"]."?nav=3\">Back to Blog</a>";
+} else {
+
+$offset=0;
+if (!empty($_GET['index']) && !is_int($_GET['index'])) {
+	$offset = 10*$_GET['index'];
+}
+
+
+	//slurp up the files
+	$files = glob($basedir . "/blog/*.post");
+	$guid = count($files);
+
+	//sort by filename
+	
+	//initialize an array to house sort results
+	$files2 = array();
+	$files2 = array_pad($files2,$guid,0);
+
+	for ($i=0; $i<$guid; $i++) {
+		$j = explode('-',basename($files[$i]));
+		$j = $j[0];
+		$j = (int)$j;
+		$j--;
+		$files2[$j] = $files[$i];
+	}
+
+	$slen = count($files2)-1;
+	$ctr=0;
+	$older=0;
+
+	for ($i=$slen-$offset; $i>-1; $i--) {
+		$shitpost=$files2[$i];
+
+		//using a counter here to know when to stop, since I don't know how many posts there will be
+		if ($ctr > 9) {
+			$older=1;
+			break;
+		}
+		$ctr++;
+
+		$statz = stat($shitpost);
+		$uid = $statz['uid'];
+		$udata = posix_getpwuid($uid);
+		$user = $udata['name'];
+
+		$date =  date("F d Y H:i:s", filemtime($shitpost));
+
+		$title = substr(strstr(basename($shitpost),'-'),1,-5);
+
+		echo "<h3 class=\"blogtitles\"><a title=permalink href=\"".$_SERVER["PHP_SELF"]."?nav=3&post=".urlencode($shitpost)."\">$title</a></h3>\n";
+		echo "<em class=\"blogdetail\">Last modified on $date UTC by $user</em><br />\n";
+		include $shitpost;
+		echo "<hr />";
+	};
+
+echo "<table width=\"100%\"><tr><td>";
+if ($older) {
+	$offset=$_GET['index']+1;
+	echo "<a href=\"".$_SERVER["PHP_SELF"]."?nav=3&index=$offset\">Older Posts</a>";
+}
+echo "</td><td style=\"text-align: right;\">";
+if (!empty($_GET['index'])) {
+	$offset=$_GET['index']-1;
+	if ($offset == 0) {
+		echo "<a href=\"".$_SERVER["PHP_SELF"]."?nav=3\">Newer Posts</a>";
+	} else {
+		echo "<a href=\"".$_SERVER["PHP_SELF"]."?nav=3&index=$offset\">Newer Posts</a>";
+	}
+}
+echo "</td></tr></table>\n";
+}
+?>

+ 1 - 0
sys/fileshare/include/blacklist.json

@@ -0,0 +1 @@
+[ "sys", "microblog", "themed" ]

+ 12 - 0
sys/fileshare/include/forbidden.inc

@@ -0,0 +1,12 @@
+<center>
+<?php
+
+$pwd = $_GET['dir'];
+
+echo '<p style="vertical-align: middle;">';
+echo '<img src="img/mime/denied.gif" alt="deeenied" />';
+echo 'Access to '.$pwd.' Denied';
+echo '<img src="img/mime/denied.gif" alt="deeniedagain" />';
+echo '</p>'
+
+?>

+ 5 - 0
sys/fileshare/include/notfound

@@ -0,0 +1,5 @@
+<p style="text-align: center; vertical-align: center;">
+ <img src="img/mime/missing.gif" alt="burritos" />
+  File not found
+ <img src="img/mime/missing.gif" alt="getdownagain" />
+</p>

+ 20 - 0
sys/fileshare/sanitize.inc

@@ -0,0 +1,20 @@
+<?php 
+if( !empty($pwd) ) {
+    //Forbid anything starting with / and anything with .. in it; also protocol links (://)
+    $forbidden = preg_match( "/|..|://", $pwd );
+    if ( $forbidden ) {
+        include 'sys/fileshare/include/forbidden.inc';
+        die();	
+    }
+
+    //Check the list of other forbidden directories
+    $blacklist = json_decode( file_get_contents("sys/fileshare/include/blacklist.json"), true );
+    if( !empty( $blacklist ) ) {
+    foreach ( $blacklist as $blacklisted ) {
+        if ( preg_match('^' . $blacklisted, $pwd) ) {
+            include 'sys/fileshare/include/forbidden.inc';
+            die();
+        }
+    }
+}
+?>

+ 17 - 0
sys/fileshare/showaudio.inc

@@ -0,0 +1,17 @@
+<?php
+//Listen to audio -- ONLY MP3s IN THIS HOUSE
+//$title = basename($post);
+ $tag = id3_get_tag($post);
+ $date =  date("F d Y H:i:s", filemtime($post));
+ $parent = dirname($post);
+ echo "<h3><a class=nudes title=permalink href=\"$post\">".$tag["title"]."</a></h3>";
+ echo "By: ".$tag["artist"]."<br />";
+ echo "$date UTC<br/ >";
+ echo "<p id=\"audioplayer_1\">Alternative content</p>";
+ echo "<script type=\"text/javascript\">\n";
+ echo "AudioPlayer.embed(\"audioplayer_1\", {soundFile: \"".$protocol."://teodesian.net/".$post."\"});\n";
+ echo "</script><br />\n";
+ echo "Description:<br />";
+ echo $tag["comment"];
+ echo "<a title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=".$icondir."tsfolder-up.gif />$parent</a><hr />";
+?>

+ 36 - 0
sys/fileshare/showcode.inc

@@ -0,0 +1,36 @@
+<?php
+//Browse text files (like code)
+$statz = stat($post);
+$uid = $statz['uid'];
+$udata = posix_getpwuid($uid);
+$user = $udata['name'];
+$date =  date("F d Y H:i:s", filemtime($post));
+$title = basename($post);
+echo "<h3 class=\"blogtitles\"><a title=permalink href=\"$post\">$title</a></h3>\n";
+echo "Last modified on $date UTC by $user<br /><br />\n";
+$text = file_get_contents($post);
+
+// Convert UTF-8 string to HTML entities
+$text = mb_convert_encoding($text, 'HTML-ENTITIES',"UTF-8");
+// Convert HTML entities into ISO-8859-1
+$text = html_entity_decode($text,ENT_NOQUOTES, "ISO-8859-1");
+// Convert characters > 127 into their hexidecimal equivalents
+for($i = 0; $i < strlen($text); $i++) {
+ $letter = $text[$i];
+ $num = ord($letter);
+ if($num>127) {
+  $out .= "&#$num;";
+ } elseif ($letter == "\n") {
+  $out .= "<br />";
+ } elseif ($letter == "\t") {
+  $out .= "&#8194;&#8194;&#8194;&#8194;";
+ } elseif ($letter == " ") {
+  $out .= "&#8194;";
+ } else {
+  $out .=  $letter;
+ }
+}
+echo $out;
+$parent = dirname($post);
+echo "<hr /><a title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=img/mime/tsfolder-up.gif />$parent</a>";
+?>

+ 15 - 0
sys/fileshare/showdoc.inc

@@ -0,0 +1,15 @@
+<?php
+// Using googgle docs until a decent PDF viewer actually exists
+// PDFObject would be nice if it didn't require a browser plugin
+$statz = stat($post);
+$uid = $statz['uid'];
+$udata = posix_getpwuid($uid);
+$user = $udata['name'];
+$date =  date("F d Y H:i:s", filemtime($post));
+$title = basename($post);
+$parent = dirname($post);
+echo "<a title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=img/mime/tsfolder-up.gif />$parent</a><hr />";
+echo "<h3 class=blogtitles><a title=permalink href=\"$post\">$title</a></h3>\n";
+echo "<em class=blogdetail>Last modified on $date UTC by $user</em><br /><br />\n";
+echo "<iframe src=\"http://docs.google.com/gview?url=http://teodesian.net/$post&embedded=true\" style=\"width:100%; min-height:500px;\" frameborder=\"0\"></iframe>";
+?>

+ 143 - 0
sys/fileshare/showfiles.inc

@@ -0,0 +1,143 @@
+<?php
+ $pwd = $_GET['dir'];
+
+ //These variables are to check whether the directory we will link to exists, and to know what directory we are in 
+ $check = @scandir($pwd.'/../', 1);
+ $splode = preg_split('[/]', $pwd, -1);
+ $predir = count($splode)-1;
+ $test = array_slice($splode, -1, 1);
+
+ #Establish MIME Type data
+ #Link is used to specify special handler URLS for particular files
+ #File specifies the link icon
+ $arch_types = array(".tar",".gz",".z7",".bz",".bz2",".zip",".rar",".lsz",
+		    "link" => "",
+		    "file" => "tsarchive.gif");
+ //Only MP3 supported for now
+ $audio_types = array(".mp3",
+		     "link" => "index.php?nav=7&post=",
+		     "file" => "tsaudio.gif");
+ $video_types = array(".ogv",
+		     "link" => "index.php?nav=8&post=",
+		     "file" => "tsmovie.gif");
+ $image_types = array(".bmp",".gif",".jpg",".jpeg",".png",".ico",".svg",".xpm",
+		     "link" => "index.php?nav=9&post=",
+		     "file" => "tsimage.gif");
+ $schematic_types = array(".dwg",".dxf",".cad",".gcode",".mcode",
+			 "link" => "",
+			 "file" => "tsschematic.gif");
+ $model_types = array(".stl",".blend",".3ds",
+		     "link" => "",
+		     "file" => "tsmodel.gif");
+ $binary_types = array(".exe",".o",".dll",".so",".jnilib",".a",".bin",".hex",
+		      "link" => "",
+		      "file" => "tssoftware.gif");
+ $code_types = array(".py",".c",".h",".js",".php",".tcl",".m",".txt",
+		    "link" => "index.php?nav=6&post=",
+		    "file" => "tscode.gif");
+ $doc_types = array(".htm",".html",".post",
+		   "link" => "index.php?nav=5&post=",
+                   "file" => "tsdoc.png");
+ $legacy_types = array(".pdf",".doc",".docx",".xls",".xlsx",".ppt",".pptx",".pages",".ai",".psd",".tiff",".tif",".eps",".ps",".xps",
+		   "link" => "index.php?nav=10&post=",
+		   "file" => "tsprop.png");
+ $mime_types = array($arch_types,$audio_types,$video_types,$image_types,$schematic_types,$model_types,$binary_types,$code_types,$doc_types,$legacy_types);
+
+ //Find the directory contents
+ $ls = @scandir($pwd, 1);
+
+// $dlist = array_filter($ls,function ($a) {return is_dir($a);});
+ $dban1 = '.';
+ $dban2 = '..';
+ $dkey = array_search($dban1,$ls);
+ if ($dkey!==false){unset($ls[$dkey]);};
+ $dkey = array_search($dban2,$ls);
+ if ($dkey!==false){unset($ls[$dkey]);};
+
+ //See if this directory is even there
+ if (@in_array($test[0], $check)) {
+	echo 'Directory listing for '.$pwd;
+	echo '<hr />';
+	$cnt = count($ls);
+    $dlist = array();
+    $flist = array();
+
+    //Trying to sort here
+    for ($n = 0; $n < $cnt; $n++) {
+        $filechk = @scandir($pwd.'/'.$ls[$n]);
+        if (! $filechk) {
+            foreach($mime_types as $mimes) {
+                foreach($mimes as $type) {
+                    $sstrn = @stristr($ls[$n],$type);
+                    if($sstrn !== FALSE && strlen($sstrn) == strlen($type)) {
+                        array_push($flist, $ls[$n]);
+                    }
+                }
+            }
+        }
+        else {
+            array_push($dlist, $ls[$n]);
+        }
+    }
+    sort($dlist);
+    sort($flist);
+    $ls = array_merge($dlist,$flist);
+	
+    //Yeah, I know this looks familiar. There's prolly some way to combine this by setting the $ikon and $link vars as an array.
+    for ($n = 0; $n < $cnt; $n++) {
+
+		//create links based on whether we are a file, and if otherwise we are a directory
+		$filechk = @scandir($pwd.'/'.$ls[$n]);
+		if (! $filechk) {
+			//default values for uncaught filetypes
+			$ikon = "tsfile.gif";
+			$link = "";
+
+			foreach ($mime_types as $mimes) {
+				foreach ($mimes as $type) {
+					//$link = $mimes["link"];
+                                        $sstrn = @stristr($ls[$n],$type);
+					if ($sstrn !== FALSE && strlen($sstrn) == strlen($type)) {
+						$ikon = $mimes["file"];
+						$link = $mimes["link"];
+						break 2;
+					};
+				};
+			};
+			echo '<img class="icon" src="/themed/'.$theme.'/img/mime/'.$ikon.'" />';
+			echo '<a href="'.$link.$pwd.'/'.$ls[$n].'">'.$ls[$n].'</a><br />';
+		}
+		else {
+			echo '<img src="/themed/'.$theme.'/img/mime/tsfolder.gif" />';
+			echo '<a href="index.php?nav=1&dir='.$pwd.'/'.$ls[$n].'">'.$ls[$n].'</a><br />'."\n";
+		}
+	}
+
+	//Figure out what the previous directory is
+	$prevdirname = '';
+
+	for ($i =0; $i < $predir; $i++) {
+
+		//We want to catch the first case, and not put a / before it
+		if ($i == 0) {
+			$prevdirname = $prevdirname.$splode[$i];
+		}
+		else {
+			$prevdirname = $prevdirname.'/'.$splode[$i];
+		}
+	}
+
+	//If we are not in the TLD, make a link to the previous dir
+	if ($prevdirname <> '') {
+
+		echo '<img src="'.$config['icondir'].'tsfolder-up.gif" />';
+		echo '<a href="index.php?nav=1&dir='.$prevdirname.'">Up one level</a>'."\n";
+
+	}
+ }
+
+ //Catch bogus directories
+ else {
+ 	include 'sys/fileshare/include/notfound';
+ }
+?> 

+ 15 - 0
sys/fileshare/showimg.inc

@@ -0,0 +1,15 @@
+<?php
+$info =  getimagesize($post);
+$ratio = round($info[0]/$info[1],2);
+$parent = dirname($post);
+if ($ratio == 1.00) {
+ echo "<img alt=\"$post\" src=\"$post\" height=100% width=100% />";
+} elseif ($ratio > 1.00) {
+ $ht = (1.20-($ratio-1))*100;
+ echo "<img alt=\"$post\" src=\"$post\" height=$ht% width=100% />";
+} else {
+ $wd = 100-(($ratio-1)*100);
+ echo "<img alt=\"$post\" src=\"$post\" style=\"padding-left: auto; padding-right: auto;\" />";
+}
+echo "<a title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=".$icondir."tsfolder-up.gif /></a>".basename($post)."&nbsp".$info[0]."x".$info[1]."<hr />";
+?>

+ 23 - 0
sys/fileshare/showpost.inc

@@ -0,0 +1,23 @@
+<?php
+//Generic .post file loader
+if ($nav == 5 && file_exists($post)) {
+ $statz = stat($post);
+ $uid = $statz['uid'];
+ $udata = posix_getpwuid($uid);
+ $user = $udata['name'];
+ $date =  date("F d Y H:i:s", filemtime($post));
+ if (stristr($post,'.') != ".post") {
+  $title = basename($post);
+ } else {
+  $title = strstr(basename($post),'.', true);
+ }
+ echo "<h3 class=\"blogtitles\"><a title=permalink href=\"index.php?nav=5&post=$post\">$title</a></h3>\n";
+ echo "<em class=\"blogdetail\">Last modified on $date UTC by $user</em><br /><hr />\n";
+ include "$post";
+ $parent = dirname($post);
+ echo "<hr /><a style=\"padding-left: auto; padding-right: auto;\" title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=img/mime/tsfolder-up.gif /></a>$parent";
+}
+
+//404 Loader for files specified in GET param that don't actually exist
+elseif ($nav == 5 && !file_exists($post)) {echo "<h1 style=\"padding-left: auto; padding-right: auto;\">404 - Not Found</h1>";}
+?>

+ 19 - 0
sys/fileshare/showvideo.inc

@@ -0,0 +1,19 @@
+<?php
+//watch movies -- Only OGV is supported
+$parent = dirname($post);
+echo "<a title=back href=\"index.php?nav=1&dir=$parent\"><img alt=back src=".$icondir."tsfolder-up.gif /></a>$parent<hr />";
+$title = basename($post);
+echo "<h3 class=blogtitles><a title=permalink href=\"$post\">".$title."</a></h3>";
+echo "<video src=\"".$post."\" type=\"video/ogg\" codecs=\"theora, vorbis\" controls=\"controls\" width=\"100%\" height=\"80%\" poster=\"img/sys/testpattern.jpg\">";
+echo "<applet code=\"com.fluendo.player.Cortado.class\" archive=\"sys/fileshare/video/cortado.jar\" width=\"100%\" height=\"80%\">";
+echo "<param name=\"url\" value=\"http://teodesian.net/".$post."\"/>\n";
+echo "<param name='bufferSize' value='4096'>\n";
+echo "<param name='bufferHigh' value='25'>\n";
+echo "<param name='bufferLow' value='5'>\n";
+echo "<param name='autoPlay' value='false'>\n";
+echo "<param name='statusHeight' value='20'>\n";
+echo "Install the Java Plugin, or Enable Scripts to see video here.<br />";
+echo "Or, <a href=\"".$post."\">download the video.\n";
+echo "</applet>";
+echo "</video>";
+?>

+ 161 - 0
sys/microblog.inc

@@ -0,0 +1,161 @@
+<?php
+  if ( !empty( $editable ) ) { //Insert the Only JS the project should have, all it does is toggle a div
+    echo "
+      <script type=\"text/javascript\">
+        function switchMenu(obj) {
+          var el = document.getElementById(obj);
+          if ( el.style.display != 'none' ) {
+            el.style.display = 'none';
+          }
+          else {
+            el.style.display = '';
+	  }
+        }
+      </script>\n";
+  }
+  $title = !empty($config['microblogtitle']) ? $config['microblogtitle'] : "Microblog";
+  echo "<p class=\"title\"><a title=\"RSS\" class=\"rss\" href=\"sys/rss/microblog.php\"></a> $title";
+  //Set important times - $tdtime is today's date, $oldtime is the oldest known date a tCMS install had nuze for - defaults to today then searches microblog dir for entries to set date
+  $tdtime = new DateTime();
+  $oldtime = clone $tdtime;
+  //limit results of directory read to first entry -- much faster than doing it with PHP once you get a large filelist. 
+  exec("ls -tr1 $basedir/microblog |head -1", $cmd_out);
+  if(!empty($cmd_out[0])) {
+    $oldtime = $oldtime = DateTime::createFromFormat('m.d.y', $cmd_out[0]);
+  }
+  $oldtime->sub(new DateInterval('P1D'));
+  /*$today and $tmrw refer to times relative to what is passed by GET params -
+  $today is the date requested by GET, $tmrw is bool designating whether $today is something other than $tdtime 
+  error indicates whether you supplied a bogus GET param for date.*/
+  $tmrw = 0;
+  $error = 0;
+  $today = clone $tdtime;
+  if(!empty($_GET["date"])) {
+    $today = DateTime::createFromFormat('m.d.y', $_GET["date"]);
+    //Catch bogus input, set $tmwr to TRUE if $today was set to something other than today's date
+    if (!filter_var($_GET["date"],FILTER_VALIDATE_REGEXP,array('options' => array('regexp' => "/^(0[1-9]|1[012])[.](0[1-9]|[12][0-9]|3[01])[.]\d\d/")))) {
+      echo "</p>That's a funny looking date you provided there, mister.\n";
+      $error=1;
+    }
+    else if ($today > $tdtime) { //catch if day supplied by GET is IN THE FUTURE
+      echo "</p>Welcome to the future<br /><img style=\"max-width:100%; padding-left: auto; padding-right: auto;\" src=\"http://gunshowcomic.com/comics/20090930.png\" />\n";
+      $error=1;
+    }   
+    else if ($today < $tdtime) {
+      $tmrw = 1;
+    }
+  }
+  /*Catch if day in question has no news -
+  If not, display day before (or before that if still no news.
+  $oldtime used here to tell when to stop looping back)*/
+  if (!$error) {
+    while (empty($todaysnews)) {
+      $todaysnews = ""; //Set it to be something empty to prevent logspam
+      $yesterday = clone $today;//This may look strange, but it'll make sense later
+      $tomorrow = clone $today;
+      $tomorrow->add(new DateInterval('P1D'));
+      $yesterday->sub(new DateInterval('P1D'));
+      //Detect if We're at the end of postings
+      if ($yesterday < $oldtime) {
+        echo " (".$today->format('m.d.y')."):</p><hr />";
+        echo "For me, it was a beginning, but for you it is the end of the road.<br /><hr />\n";
+        if ($oldtime != $tdtime) {
+          $tomorrow = clone $oldtime;
+          $tomorrow->add(new DateInterval('P1D'));
+	  $tomorrow = $tomorrow->format('m.d.y');
+        }
+        $todaysnews = "end";
+      }
+      if ($todaysnews != "end") {
+        //Get news from directory if any exists for that day, glob will return empty if nothing is in dir
+        $todaysnews = glob("$basedir/microblog/".$today->format('m.d.y')."/*");
+        //Set display date for today's news, set $today to be yesterday in order to get while loop to recurse correctly
+        $realtime = $today->format('m.d.y');
+        if(!empty($_GET['fwd']) && $_GET['fwd']) {//Check whether we are traversing forward or backward in time
+          $today = clone $tomorrow;
+        } else { //Default to going back
+          $today = clone $yesterday;
+        }
+        //Finish by setting times for Yesterday and Tomorrow so that they can be used in links below
+        $tomorrow = clone $yesterday;
+        $tomorrow->add(new DateInterval('P2D'));
+        $tomorrow = $tomorrow->format('m.d.y');
+        $yesterday = $yesterday->format('m.d.y');
+      }
+    }
+    if ($todaysnews != "end") {
+      echo " (".$realtime."):</p><hr />\n";
+      foreach ($todaysnews as $i) {
+        $fh = fopen($i,'r');
+        $fc = fread($fh,10000); //If a microblog item is more than 1kb, you are doing something wrong.
+        fclose($fh);
+        $json = json_decode($fc);
+        if(is_null($json)) {
+          echo $fc;
+        } elseif (!empty($json->title) && !empty($json->url) && !empty($json->poster)) {
+          $out = '<h3 class="blogtitles">
+                    <a href="'.$json->url.'">'.$json->title.'</a>
+                    <a class="usericon '.$json->poster.'" title="Posted by '.$json->poster.'"></a>
+                  </h3>';
+          if(!empty($json->image)) {
+            $out .= '<img class="mblogimg" src="'.$json->image.'" />';
+          }
+          if(!empty($json->audio)) {
+            $out .= '<audio src="'.$json->audio.'" controls>
+                       Download Audio 
+                       <a href="'.$json->audio.'">Here</a><br />
+                     </audio>';
+          }
+          if(!empty($json->video)) {
+            $out .= '<video src="'.$json->video.'" controls>
+                       Download Video
+                       <a href="'.$json->video.'">Here</a><br />
+                     </video>';
+          }
+          if(!empty($json->comment)) {
+            $out .= $json->comment;
+          }
+          $out .= '<hr />';
+          echo $out;
+        } #Note that if nothing works out here, I'm just opting not to show anything.
+        if ( !empty( $editable ) ) {
+          $id=basename($i);
+		  $editblock = "
+            <a style=\"display: inline-block;\" onclick=\"switchMenu('$id');\">[Edit]</a>
+            <div style=\"display: none;\" id=\"$id\">
+             <form style=\"display: inline\" method=\"POST\">
+              <input type=\"hidden\" name=\"id\" value=\"$i\" />
+              <input type='hidden' name='action' value='Edit' />";
+          if(is_null($json)) {
+            $editblock .= "<textarea class=\"mbedit_text\" name=\"content\">$fc</textarea>";
+          } else {
+          $editblock .= '<input type="hidden" name="type" value="JSON" />
+            Title: <input class="cooltext" type="text" name="title"
+			value="' . str_replace( '"' ,'&quot;' , $json->title ) .'" /><br />
+            URL: <input class="cooltext" type="text" name="URL" value="'.$json->url.'" /><br />
+            Image: <input class="cooltext" type="text" name="IMG" value="'.$json->image.'" /><br />
+            Audio: <input class="cooltext" type="text" name="AUD" value="'.$json->audio.'" /><br />
+            Video: <input class="cooltext" type="text" name="VID" value="'.$json->video.'" /><br />
+            Comments: <textarea class="cooltext" name="comment">'.$json->comment.'</textarea>';
+          }
+          $editblock .= "<input type=\"hidden\" name=\"app\" value=\"microblog\" />
+             <input class=\"coolbutton mbedit_button\" type=\"submit\" Value=\"Edit\" />
+             </form>
+             <form style=\"display: inline\" method=\"POST\">
+              <input type=\"hidden\" name=\"app\" value=\"microblog\" />
+              <input type=\"hidden\" name=\"id\" value=\"$i\" />
+              <input type='hidden' name='action' value='Delete' />
+              <input class=\"coolbutton mbedit_button\" type=\"submit\" value=\"Delete\" />
+             </form>
+            </div>
+            <hr class=\"clear\" />";
+          echo $editblock;
+        }
+      }
+      echo "<a style=\"float: left;\" title=\"skips empty days\" href=\"?nav=2&date=".$yesterday."&fwd=0\">Older Entries</a>\n";
+    }
+    if ($tmrw) {
+      echo "<a style=\"float: right;\" href=\"?nav=2&date=".$tomorrow."&fwd=1\">Newer Entries</a>\n";
+    }
+  }
+?>

+ 71 - 0
sys/rss/blog.php

@@ -0,0 +1,71 @@
+<?php
+  header('Content-Type: application/rss+xml'); 
+  if (!empty($_SERVER["HTTPS"])) {
+    $protocol = "http";
+  } else {
+    $protocol = "https";
+  }
+	extract(json_decode(file_get_contents('../admin/config/main.json'),true));
+    echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+	echo "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n";
+	echo "<channel>\n";
+    $atomlink = "$protocol://".$_SERVER["SERVER_NAME"]."/".$basedir.$rssdir."blog.php";
+	echo "<atom:link href=\"".$atomlink."\" rel=\"self\" type=\"application/rss+xml\" />";
+	echo "\t<title>".$htmltitle."</title>\n";
+	echo "\t<description>".$blogtitle."</description>\n";
+	echo "\t<link>http://".$_SERVER["SERVER_NAME"]."/".$basedir."</link>\n";
+
+	$tiem = date(DATE_RFC2822, time());
+
+	echo "\t<lastBuildDate>$tiem</lastBuildDate>\n";
+	echo "\t<pubDate>$tiem</pubDate>\n";
+
+	$files = glob($_SERVER["DOCUMENT_ROOT"]."/".$basedir.$blogdir."*.post");
+	$guid = count($files);
+
+	//sort by filename
+	
+	//initialize an array to house sort results
+	$files2 = array();
+	$files2 = array_pad($files2,$guid,0);
+
+	for ($i=0; $i<$guid; $i++) {
+		$j = explode('-',basename($files[$i]));
+		$j = $j[0];
+		$j = (int)$j;
+		$j--;
+		$files2[$j] = $files[$i];
+	}
+
+	$slen = count($files2)-1;
+	$ctr = 0;
+
+		for ($i=$slen; $i>-1; $i--) {
+			$shitpost=$files2[$i];
+		
+			if ($ctr > 9) {break;};
+			$ctr++;
+
+                	$statz = stat($shitpost);
+                	$uid = $statz['uid'];
+                	$udata = posix_getpwuid($uid);
+                	$user = $udata['name'];
+
+                	$date =  date(DATE_RFC2822, filemtime($shitpost));
+
+                	$title = substr(strstr(basename($shitpost),'-'),1,-5);
+			$contents = file_get_contents($shitpost);
+
+			echo "\t<item>\n";
+               		echo "\t\t<title>$title</title>\n";
+                	echo "\t\t<description><![CDATA[".$contents."]]>\t\t</description>\n";
+			echo "\t\t<link>http://teodesian.net/index.php?nav=8&amp;post=".$shitpost."</link>\n";
+			echo "\t\t<guid isPermaLink=\"false\">$guid-teodesian.net</guid>\n";
+			echo "\t\t<pubDate>".$date."</pubDate>\n";
+			echo "\t\t<author>".$user."</author>\n";
+			echo "\t</item>\n";
+			$guid--;
+		}
+	echo "</channel>\n";
+	echo "</rss>";
+?>

+ 78 - 0
sys/rss/microblog.php

@@ -0,0 +1,78 @@
+<?php
+  header('Content-Type: application/rss+xml'); 
+  if (!empty($_SERVER["HTTPS"])) {
+    $protocol = "http";
+  } else {
+    $protocol = "https";
+  }
+  //Import your config, set some stuff up, then construct the mining laser
+  extract(json_decode(file_get_contents('../admin/config/main.json'),true));
+  $tcmsUsers = json_decode(file_get_contents('../admin/config/users.json'),true);
+  date_default_timezone_set($timezone);
+  $tiem = date(DATE_RSS);
+  $today = date("m.d.y");
+  $atomlink = "$protocol://".$_SERVER["SERVER_NAME"]."/".$basedir.$rssdir."microblog.php";
+  $newsdir = $_SERVER["DOCUMENT_ROOT"]."/".$basedir.$microblogdir;
+  $files = glob($newsdir.$today."/*");
+  $slen = count($files);
+  $feed = '<?xml version="1.0" encoding="UTF-8"?>
+            <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
+	     <channel>
+	      <atom:link href="'.$atomlink.'" rel="self" type="application/rss+xml" />
+	      <title>'.$htmltitle.'</title>
+	      <description>'.$microblogtitle.' RSS Feed</description>
+	      <link>http://'.$_SERVER['SERVER_NAME'].'/'.$basedir.'</link>
+	      <lastBuildDate>'.$tiem.'</lastBuildDate>
+	      <pubDate>'.$tiem.'</pubDate>';
+  foreach ($files as $shitpost) {
+    $storyPubDate =  date(DATE_RSS, strtotime(basename($shitpost)));
+    $contents = file_get_contents($shitpost);
+    #Set some sane defaults for cases where no user exists
+    $email = "null@example.com";
+    $author = "X";
+    #Check the format, do needful based on what's here
+    $json = json_decode($contents);
+    if(is_null($json)) {
+      //HAHAHA You thought you needed an XML parser, didn't you?
+      $theRipper = explode("<",$contents);
+      $theRipper = explode(">",$theRipper[2]);
+      $storyTitle = $theRipper[1];
+      $theRipper = explode('"',$theRipper[0]);
+      $storyLink = htmlspecialchars($theRipper[1]);
+      $theRipper = explode("</h3>",$contents);
+      $theRipper = explode("<hr />",$theRipper[1]);
+      $storyText = $theRipper[0];
+      $theRipper = explode("title=\"Posted by ",$contents);
+      $theRipper = explode('"',$theRipper[1]);
+      $poster = $theRipper[0];
+      if(isset($tcmsUsers[$poster])) {
+          $email = $tcmsUsers[$poster]["email"];
+          $author = $tcmsUsers[$poster]["fullName"]; 
+      }
+      $feed .= '<item>
+                 <title>'.$storyTitle.'</title>
+                 <description><![CDATA['.$storyText.']]></description>
+                 <link>'.preg_replace('/&/', '&#038;', $storyLink).'</link>
+                 <guid isPermaLink="false">'.basename($shitpost).'-'.$_SERVER["SERVER_NAME"].'</guid>
+                 <pubDate>'.$storyPubDate.'</pubDate>
+                 <author>'.$email.' ('.$author.')</author>
+                </item>';
+    } elseif (!empty($json->title) && !empty($json->url) && !empty($json->poster)) {
+        if(isset($tcmsUsers[$json->poster])) {
+            $email = $tcmsUsers[$json->poster]["email"];
+            $author = $tcmsUsers[$json->poster]["fullName"]; 
+        }
+        $feed .= '<item>
+                   <title>'.$json->title.'</title>
+                   <description><![CDATA['.$json->comment.']]></description>
+                   <link>'.preg_replace('/&/', '&#038;', $json->url).'</link>
+                   <guid isPermaLink="false">'.basename($shitpost).'-'.$_SERVER["SERVER_NAME"].'</guid>
+                   <pubDate>'.$storyPubDate.'</pubDate>
+                   <author>'.$email.' ('.$author.')</author>
+                  </item>';
+    }
+  }
+  $feed .= ' </channel>
+            </rss>';
+  print_r($feed);
+ ?>

TEMPAT SAMPAH
www/img/avatar/humm.gif


TEMPAT SAMPAH
www/img/avatar/hydra-lel.png


TEMPAT SAMPAH
www/img/avatar/naptime.png


TEMPAT SAMPAH
www/img/icon/rss.png


File diff ditekan karena terlalu besar
+ 112 - 0
www/img/icon/tCMS.svg


TEMPAT SAMPAH
www/img/mime/denied.gif


TEMPAT SAMPAH
www/img/mime/missing.gif


TEMPAT SAMPAH
www/img/mime/tsarchive.gif


TEMPAT SAMPAH
www/img/mime/tsaudio.gif


TEMPAT SAMPAH
www/img/mime/tscode.gif


TEMPAT SAMPAH
www/img/mime/tsdoc.png


TEMPAT SAMPAH
www/img/mime/tsdownload.gif


TEMPAT SAMPAH
www/img/mime/tsfile.gif


TEMPAT SAMPAH
www/img/mime/tsfolder-up.gif


TEMPAT SAMPAH
www/img/mime/tsfolder.gif


TEMPAT SAMPAH
www/img/mime/tsimage.gif


TEMPAT SAMPAH
www/img/mime/tsmodel.gif


TEMPAT SAMPAH
www/img/mime/tsmovie.gif


TEMPAT SAMPAH
www/img/mime/tsprop.png


TEMPAT SAMPAH
www/img/mime/tsschematic.gif


TEMPAT SAMPAH
www/img/mime/tssoftware.gif


TEMPAT SAMPAH
www/img/mime/tssticky.gif


TEMPAT SAMPAH
www/img/sys/testpattern.jpg


+ 0 - 0
www/scripts/main.js


+ 140 - 0
www/server.psgi

@@ -0,0 +1,140 @@
+use strict;
+use warnings;
+
+no warnings 'experimental';
+use feature qw{signatures};
+
+use Date::Format qw{strftime};
+
+use URL::Encode  ();
+use Text::Xslate ();
+use Plack::MIME  ();
+use Mojo::File   ();
+use DateTime::Format::HTTP();
+use Encode qw{encode_utf8};
+
+#Grab our custom routes
+use lib 'lib';
+use Trog::Routes::HTML;
+use Trog::Routes::JSON;
+
+# Troglodyne philosophy - simple as possible
+
+# Import the routes
+my %routes = %Trog::Routes::HTML::routes;
+@routes{keys(%Trog::Routes::JSON::routes)} = values(%Trog::Routes::JSON::routes);
+
+# Things we will actually produce from routes rather than just serving up files
+my $ct = 'Content-type';
+my %content_types = (
+    plain => "$ct:text/plain;",
+    html  => "$ct:text/html; charset=UTF-8",
+    json  => "$ct:application/json;",
+    blob  => "$ct:application/octet-stream;",
+);
+
+my $cd = 'Content-disposition';
+my %content_dispositions = (
+    attachment => 'attachment; filename=',
+    inline     => 'inline; filename=',
+);
+
+my $cc = 'Cache-control';
+my %cache_control = (
+    revalidate => "$cc: no-cache, max-age=0;",
+    nocache    => "$cc: no-store;",
+    static     => "$cc: public, max-age=604800, immutable",
+);
+
+my $app = sub {
+    my $env = shift;
+
+    my $last_fetch = 0;
+    if ($env->{HTTP_IF_MODIFIED_SINCE}) {
+        $last_fetch = DateTime::Format::HTTP->parse_datetime($env->{HTTP_IF_MODIFIED_SINCE})->epoch();
+    }
+
+    my $query = {};
+    $query = URL::Encode::url_params_mixed($env->{QUERY_STRING}) if $env->{QUERY_STRING};
+    my $path = $env->{PATH_INFO};
+
+    #Disallow any paths that are naughty ( starman auto-removes .. up-traversal)
+    if (index($path,'/templates') == 0 || $path =~ m/.*\.psgi$/i ) {
+        return [ 403, [$content_types{plain}], ["STAY OUT YOU RED MENACE"]];
+    }
+
+    # If it's just a file, serve it up
+    return _serve("www/$path", $last_fetch) if -f "www/$path";
+
+    #TODO reject inappropriate content-lengths
+    return [ 404, [$content_types{plain}], ["RETURN TO SENDER"]] unless exists $routes{$path};
+    return [ 400, [$content_types{plain}], ["BAD REQUEST"]] unless $routes{$path}{method} eq $env->{REQUEST_METHOD};
+
+    #TODO fish out what user is in cookie
+    $query->{user} = 'Nobody';
+
+    my $output =  $routes{$path}{callback}->($query,$env->{input}, \&_render);
+    return $output;
+};
+
+sub _serve ($path, $last_fetch=0) {
+    my $mf = Mojo::File->new($path);
+    my $ext = '.'.$mf->extname();
+    my $ft;
+    $ft = Plack::MIME->mime_type($ext) if $ext;
+    $ft = "$ct:$ft;" if $ft;
+    $ft ||= $content_types{plain};
+
+    my @headers = ($ft);
+
+    #TODO figure out content-disposition
+
+    #TODO use static Cache-Control for everything but JS/CSS?
+    push(@headers,$cache_control{revalidate});
+
+
+    #TODO Return 304 unchanged for files that haven't changed since the requestor reports they last fetched
+    my $mt = (stat($path))[9];
+    my @gm = gmtime($mt);
+    my $now_string = strftime( "%a, %d %b %Y %H:%M:%S GMT", @gm );
+    my $code = $mt > $last_fetch ? 200 : 304;
+
+    push(@headers, "Last-Modified: $now_string\n");
+
+    my $h = join("\n",@headers);
+    if (open(my $fh, '<', $path)) {
+        return [ $code, [$h], $fh];
+    }
+    return [ 403, [$content_types{plain}], ["STAY OUT YOU RED MENACE"]];
+}
+
+sub _render ($template, $vars) {
+
+    my $processor = Text::Xslate->new(
+        path   => 'www/templates',
+        header => ['header.tx'],
+        footer => ['footer.tx'],
+    );
+
+    #XXX default vars that need to be pulled from config
+    $vars->{dir}       //= 'ltr';
+    $vars->{lang}      //= 'en-US';
+    $vars->{title}     //= 'tCMS';
+    #XXX Need to have minification detection and so forth, use LESS
+    $vars->{stylesheets}  //= [];
+    #XXX Need to have minification detection, use Typescript
+    $vars->{scripts} //= [];
+
+    $vars->{contenttype} //= $content_types{html};
+    $vars->{cachecontrol} //= $cache_control{revalidate};
+
+    my @headers = ($vars->{contenttype});
+    push(@headers,$vars->{contentdisposition}) if $vars->{contentdisposition};
+    push(@headers, $vars->{cachecontrol}) if $vars->{cachecontrol};
+    my $h = join("\n",@headers);
+
+    my $body = $processor->render($template,$vars);
+    return [200, [$h], [encode_utf8($body)]];
+}
+
+

+ 6 - 0
www/styles/avatars.css

@@ -0,0 +1,6 @@
+/*User Images set here*/
+a.Nobody {
+ background-image: url(/img/avatar/humm.gif);
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/img/avatar/hydra.png', sizingMethod='scale');
+ -ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/img/avatar/hydra.png', sizingMethod='scale')"
+}

+ 63 - 0
www/styles/login.css

@@ -0,0 +1,63 @@
+body {
+    background-color: gray;
+    font-family: sans-serif;
+}
+#login {
+    margin: 0 auto;
+    max-width: 25rem;
+    color: white;
+}
+#logo {
+    display: block;
+    max-width: 90%;
+    margin: 0 0 0 5%;
+    height: 2rem;
+}
+#login form {
+    max-width: 85%;
+    display: block;
+    margin: 0 auto;
+}
+#maximumGo {
+    width: calc(100% - 1rem);
+}
+#copyright {
+    font-size: .75rem;
+    text-align: center;
+}
+#jsalert {
+    margin: 1rem;
+    border-radius: .75rem;
+    border-color: rgba(255,0,0,.75);
+    padding: 1rem;
+}
+.alert-danger {
+    background-color: rgba(255,0,0,.25);
+}
+.alert-success {
+    background-color: rgba(0,255,0,.25);
+}
+input {
+    box-sizing: border-box;
+    border-radius: .5em;
+    border: .25em solid black;
+    color: white;
+    padding: .25em;
+    margin: .25em;
+}
+.input-group {
+    display: table;
+    width: 100%;
+}
+.input-group > input, .input-group > label {
+    display: table-cell;
+}
+.input-group > input {
+    width: calc(100% - 1rem);
+    background-color: #333;
+}
+input[type="submit"] {
+    box-shadow: 0 0 .5em black;
+    background-color: #333;
+    color: white;
+}

+ 39 - 0
www/styles/notconfigured.css

@@ -0,0 +1,39 @@
+body, html {
+    font-size: 100%;
+    margin: 0;
+    background-color:white;
+}
+nav {
+    padding: .5rem;
+    height: 2rem;
+    line-height: 2rem;
+    font-size: 1.5rem;
+    background-color: black;
+    color: white;
+}
+section {
+    display: block;
+    margin: 1rem auto 0 auto;
+    padding: 0 1rem;
+}
+#notice {
+    display: table;
+    padding: .5rem;
+    background-color: rgba( 0, 0, 0, .75 );
+    color: #00FF00;
+    border-radius: .25rem;
+}
+#notice > img, #notice > span {
+    margin: .25rem;
+    display: table-cell;
+    vertical-align: middle;
+}
+/* Styles for larger viewports */
+@media( min-width: 768px ) {
+    section {
+        width: 80%;
+    }
+    #notice {
+        width: calc( 100% - 1rem);
+    }
+}

+ 34 - 0
www/styles/print.css

@@ -0,0 +1,34 @@
+#littlemenu, #topkek, #leftbar, #rightbar, #footbar {
+ display: none !important;
+}
+#kontent {
+ text-align: left;
+ font-family: sans-serif;
+ font-size: 12pt;
+}
+p.title {
+ font-weight: bold;
+}
+ul {
+ list-style-type: disc;
+}
+table {
+ border-spacing: 0px;
+}
+td {
+ padding: 0px;
+}
+pre {
+ font-family: sans-serif;
+ font-size: 12pt;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ white-space: pre-wrap;
+}
+h3.blogtitles {
+ margin-bottom: 5px;
+ margin-bottom: 5px;
+}
+em.blogdetail {
+ font-size: 8pt;
+}

+ 290 - 0
www/styles/screen.css

@@ -0,0 +1,290 @@
+/*SCREEN.CSS - All styling for the screen view of the site should be done here*/
+
+/*Global tag based changes go here*/
+a {
+ color: rgb(0,0,0);
+ border: 0px;
+}
+body {
+ color: black;
+ background-color: gray;
+ font-family: sans-serif;
+ font-size: 100%;
+}
+img {
+ border: 0px;
+}
+table {
+ border-spacing: 0px;
+}
+td {
+ padding: 0px;
+ margin: 0px;
+}
+hr {
+ width: 100%
+}
+ul {
+ list-style-type: disc;
+}
+pre {
+ margin-top: .25em;
+ margin-bottom: .25em;
+ white-space: pre-wrap;
+ font-family: sans-serif;
+ font-size: 1em;
+}
+textarea {
+ height: 10em;
+ width: 99%;
+ background-color: #333;
+ height: 15em;
+}
+audio, video {
+ display: block;
+}
+
+/*Major DOM Element Styling goes below*/
+#topkek {
+ background: rgb(0,0,0);
+ box-shadow: 0 .25em .5em black;
+}
+.toplel {
+ color: white;
+ vertical-align: middle;
+}
+#lefttitle {
+ font-family: courier;
+ font-size: 1em;
+ font-weight: bold;
+}
+#midtitle {
+ text-align: center;
+}
+#righttitle {
+ text-align: right;
+}
+#righttitle a, #configbar a {
+ padding-right: .5em;
+ border-right: .1em #333 solid;
+}
+#righttitle a:last-child, #configbar a:last-child {
+ border-right: 0;
+}
+#menubutton {
+ vertical-align: middle;
+}
+#littlemenu {
+ background-color: rgb(0,0,0);
+ background-color: rgba(0,0,0,.75);
+ border-bottom-left-radius: 1em;
+}
+#kontent {
+ background: rgb(255,255,255); /*IE Fallback*/
+ background: rgba(255,255,255,.90);
+ border-radius: 0px 0px 1em 1em;
+ box-shadow: 0 .5em 1em black;
+}
+#footbar {
+ color: white;
+ background-color: black;
+ text-align: center;
+}
+/* admin styles */
+#mbengine {
+ width: 100%;
+ display: table;
+}
+#submissions {
+ width: 20%;
+ display: table-cell;
+}
+#stories {
+ vertical-align: top;
+}
+
+/*Icon/Button styles below*/
+.rss {
+ border: 0px;
+ height: 1em;
+ width: 1em;
+ background-size: 1em;
+ background-image: url(/img/icon/rss.png);
+ display: inline-block;
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/img/icon/rss.png', sizingMethod='scale');
+ -ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/img/icon/rss.png', sizingMethod='scale')";
+}
+.usericon, .buddyicon {
+ width: 1em;
+ height: 1em;
+ float: right;
+ background-size: 1em;
+ background-repeat: no-repeat;
+ margin-left:1rem;
+}
+.avatar {
+ width: 3em;
+ height: 3em;
+ float: left;
+ background-size: 3em;
+}
+button#clickme {
+ display: none;
+ float: right;
+ box-shadow: 0px 0px 0.5em #66CCFF;
+ padding: 0 .25em;
+ margin: .25em;
+ font-size: .60em;
+ background-color: #333;
+ border-radius: .5em;
+ border: .25em solid black;
+ color: red;
+}
+button#clickme:active {
+ padding-left: .30em;
+ border-color: gray;
+}
+.coolbutton, .cooltext, textarea {
+ box-sizing: border-box;
+ border-radius: .5em;
+ border: .25em solid black;
+ color: white;
+ padding: .25em;
+ margin: .25em;
+}
+.coolbutton {
+ box-shadow: 0 0 .5em black;
+ background-color: #333;
+}
+.coolbutton:active {
+ padding-left: .30em;
+ border-color: gray;
+}
+.cooltext {
+ background-color: #333;
+ width: 100%;
+}
+#Submissions input, #Submissions textarea {
+ width: 95%;
+ display: block;
+ margin-right: auto;
+ margin-left: auto;
+}
+#Submissions textarea {
+ height: 20em;
+ vertical-align: top;
+}
+
+/*Various other stylistic stuff below*/
+a.nudes {
+ color: rgb(0,255,0);
+}
+img.icon {
+ height: 1em;
+ width: 1em;
+}
+img.mblogimg, {
+ max-width: 100%;
+ display: block;
+}
+h3.blogtitles {
+ margin-bottom: .5em;
+ margin-top: 0px;
+}
+em.blogdetail {
+ font-size: .8em;
+}
+a.topbar {
+ text-decoration: none;
+ color: white;
+}
+a:hover.topbar {
+ text-decoration: underline;
+}
+img.titlebar {
+ height: 1.5em;
+ float: left;
+}
+p.title {
+ padding-top: 0px;
+ margin-top: 0px;
+ font-weight: bold;
+}
+p#linkcontainer {
+ padding: 2em .5em .5em .5em;
+ background: rgb(0,0,0); /*IE Fallback*/
+ background: rgba(0,0,0,.75);
+ border-bottom-left-radius: 1em;
+ margin-top: 0px;
+}
+span.bold {
+ font-weight: bold;
+}
+p.posteditortitle {
+ margin: 0px;
+ font-weight: bold;
+ border-bottom: .1em solid gray;
+ padding: .5em;
+}
+.disabled {
+ display: none;
+}
+.clear {
+ clear: both;
+}
+.mbedit_text {
+ display: inline-block;
+}
+.mbedit_button {
+ width: 5em;
+ display: inline-block;
+}
+#newposttitle {
+ width: 95%;
+ display: block;
+ box-sizing: border-box;
+}
+#newpostlink {
+ color: #990000;
+}
+/*Responsive design stuff that used to be in JS, modify as needed*/
+@media (max-width: 1024px) {
+  #lefttitle {
+    width: 100%;
+    max-width: 100%;
+  }  
+  #clickme {
+    display: table-cell !important;
+  }
+  #righttitle, #configbar {
+    visibility: hidden;
+    position: fixed;
+    top: 2rem;
+    right: 0;
+    min-width: 0;
+    max-width: 100%;
+    background-color: rgba(0,0,0,.75);
+    border-bottom-left-radius: 1em;
+  }
+  #clickme:active ~ #righttitle, #clickme:focus ~ #righttitle, #righttitle:hover, #righttitle a:active, #clickme:active ~ #configbar, #clickme:focus ~ #configbar, #configbar:hover, #configbar a:active  {
+    visibility: visible;
+  }
+  #righttitle a, #configbar a {
+    display: block;
+    border-right: 0;
+  }
+}
+@media (max-width: 700px) {
+  #mbengine {
+   width: 100%;
+   display: block;
+  }
+  #submissions {
+   width: 100%;
+   display: block;
+  }
+}
+.manageUserEntry {
+    border: .25rem dashed black;
+    padding: .5rem;
+}

+ 96 - 0
www/styles/structure.css

@@ -0,0 +1,96 @@
+/*First, we start off with specifying what parts of the DOM we want hidden*/
+#leftbar, #rightbar, #footbar, #midtitle {
+ display: none;
+}
+
+/*Now, on to positioning of the elements*/
+body {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ min-height: 100vh;
+ max-height: 100%;
+}
+#topkek {
+ display: table;
+ position: fixed;
+ z-index: 2;
+ top: 0;
+ width: 100%;
+ height: 2rem;
+}
+.toplel {
+ display: table-cell;
+}
+#lefttitle {
+ padding-left: .5em;
+ max-width: 50%;
+ min-width: 33%;
+}
+#midtitle {
+ width: 33%;
+}
+#righttitle {
+ min-width: 33%;
+ max-width: 50%;
+ padding-right: .5em;
+}
+#righttitle a, #configbar a {
+ display: inline-block;
+}
+#menubutton {
+ display: none;
+ min-width: 2%;
+ max-width: 33%;
+ padding-right: .5em;
+}
+#littlemenu {
+ display: none;
+ position: fixed;
+ z-index: 3;
+ top: 1.5em;
+ right: 0px;
+ padding: 0px .5em .5em .5em;
+}
+#littlemenu a {
+ display: block;
+}
+#kontainer {
+ display: table;
+ width: 100%;
+ padding-bottom: 2.5em;
+}
+.kontained {
+ display: table-cell;
+}
+#leftbar {
+ width: 6em;
+ padding-right: .5em;
+}
+#kontent {
+ padding: 3em .5em .5em .5em;
+ min-height: 50%;
+ max-width: 100%;
+ margin-top: 0px;
+ width:100%;
+}
+#rightbar {
+ width: 6em;
+ padding-left: .5em;
+}
+#footbar {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 1.5em;
+}
+#stories {
+ padding: 0px .5em .5em .5em;
+ width: 80%;
+ display: table-cell;
+}
+
+/*Some CSS rules below that I actually want to load on both print and screen views*/
+blockquote {
+  font-style: italic;
+}

+ 41 - 0
www/templates/config.tx

@@ -0,0 +1,41 @@
+<p class="title">
+ General settings:
+</p>
+<hr />
+<form id="mainConfig" method="post" action="/config/save">
+    TODO configuration form here
+    <br />
+    <input type="submit" class="coolbutton" value="Commit Changes" />
+</form>
+<hr />
+
+<p class="title">
+ User management:
+</p>
+<hr />
+<form id="userConfig" method="post" action="/posts/save">
+    TODO include the posts/ view here to show all users so we can b&
+    <br />
+    <input type="submit" class="coolbutton" value="Commit Changes" />
+</form>
+<br />
+<p class="title">
+ Add User:
+</p>
+<form id="addUser" method="post" action="index.php">
+    TODO include the /post view here so we can add new user entries which *don't* have a domain email account
+    <br />
+    <input type="submit" class="coolbutton" value="Commit Changes" />
+</form>
+<hr />
+
+<p class="title">
+ Theme cloner:
+</p>
+<hr />
+<p>
+  Want to write your own theme?
+  Clone a theme here then see the <a href="https://tcms.troglodyne.net/index.php?nav=5&post=fileshare/manual/Chapter 03-Customization.post" title="GET UR MIND RITE">styling guide</a>
+  for information on how tCMS' templates, image sets and CSS work in the theming system.
+</p>
+INSERT FORM HERE

+ 1 - 0
www/templates/default.tx

@@ -0,0 +1 @@
+Welcome to tCMS!

+ 1 - 0
www/templates/footbar.tx

@@ -0,0 +1 @@
+POTZREBIE

+ 2 - 0
www/templates/footer.tx

@@ -0,0 +1,2 @@
+    </body>
+</html>

+ 20 - 0
www/templates/header.tx

@@ -0,0 +1,20 @@
+<!doctype html>
+<html dir="<: $dir :>" lang="<: $lang :>">
+    <head>
+        <title><: $title :></title>
+        <meta charset="utf-8" />
+        <link rel="icon" type="image/vnd.microsoft.icon" href="<: $theme_dir :>/img/icon/favicon.ico" />
+        <meta name="description" content="A Simple CMS by teodesian.net"/>
+        <meta name="viewport" content="width=device-width">
+        <link rel="stylesheet" type="text/css" href="styles/structure.css" />
+        <link rel="stylesheet" type="text/css" href="styles/screen.css" media="screen" />
+        <link rel="stylesheet" type="text/css" href="styles/print.css" media="print" />
+        <!-- Stylesheets !-->
+        : for $stylesheets -> $stylesheet {
+        <link rel="stylesheet" type="text/css" href="<: $stylesheet :>" media="screen" />
+        : }
+        : for $scripts -> $script {
+        <script type="text/javascript" src="scripts/<: $script :>"></script>
+        : }
+    </head>
+    <body>

+ 35 - 0
www/templates/index.tx

@@ -0,0 +1,35 @@
+<div id="topkek">
+    <div id="lefttitle" class="toplel">
+        <: $htmltitle | mark_raw :>
+    </div>
+    <div id="midtitle" class="toplel">
+        <: $midtitle | mark_raw :>
+    </div>
+    <button title="Menu" id="clickme">&#9776;</button>
+    <div id="righttitle" class="toplel">
+        <a href="/news"  title="Micro Blog" class="topbar">LinkLog</a>
+        <a href="/blog"  title="Blog" class="topbar">Blog</a>
+        <a href="/wiki"  title="Wiki" class="topbar">Wiki</a>
+        <a href="/video" title="Video" class="topbar">Video</a>
+        <a href="/audio" title="Audio" class="topbar">Audio</a>
+        <a href="/files" title="Files" class="topbar">Files</a>
+        <a href="/about" title="About" class="topbar">About</a>
+        <a href="/config" title="config" class="topbar usericon <: $user :>"></a>
+    </div>
+</div>
+<div id="littlemenu">
+</div>
+<div id="kontainer">
+    <div id="leftbar" class="kontained">
+      <: $leftbar | mark_raw :>
+    </div>
+    <div id="kontent" class="kontained">
+        <: $content | mark_raw :>
+    </div>
+    <div id="rightbar" class="kontained">
+        <: $rightbar | mark_raw :>
+    </div>
+</div>
+<div id="footbar">
+    <: $footbar | mark_raw :>
+</div>

+ 1 - 0
www/templates/leftbar.tx

@@ -0,0 +1 @@
+POTZREBIE

+ 57 - 0
www/templates/login.tx

@@ -0,0 +1,57 @@
+<script>
+    document.addEventListener("DOMContentLoaded", function(event) {
+        var loginFailure = <: $login_failure :>;
+        if( loginFailure === -1 ) {
+            document.querySelector('#jsalert').style.cssText = 'visibility: hidden;';
+        } else if ( loginFailure === 1 ) {
+            document.querySelector('#jsalert').classList.remove("alert-success");
+            document.querySelector('#jsalert').classList.add("alert-danger");
+            document.querySelector('#msgIcon').innerHTML = "❌";
+            document.querySelector('#message').innerHTML = "<: $login_message :>";
+        } else {
+            document.querySelector('#jsalert').classList.remove("alert-danger");
+            document.querySelector('#jsalert').classList.add("alert-success");
+            document.querySelector('#msgIcon').innerHTML = "✓";
+            document.querySelector('#message').innerHTML = "<: $login_message :>";
+            //JS redir better ux than 302
+            window.setTimeout(function() {
+              window.location="<: $to :>";
+            }, 500);
+        }
+    });
+</script>
+<div id="login">
+    <div id="jsalert" class="alert-danger">
+      <table>
+        <tr>
+          <td id="msgIcon">
+            ⚠
+          </td>
+          <td id="message" style="padding-left: 1rem;">
+            Please enable JavaScript on this domain.
+          </td>
+        </tr>
+      </table>
+    </div>
+    <div>
+      <img id="logo" src="img/icon/tCMS.svg" style="float:left" /><span style="font-family:courier;font-size:2rem;">CMS Login</span>
+    </div>
+    <div id="spacer" style="clear: both;"><br /></div>
+    <form method="POST" action="/auth">
+      <input type="hidden" name="app" value="login" />
+      <input type="hidden" name="to" value="<: $to :>" />
+      Username<br />
+      <div class="input-group">
+        <label for="username">😎</span></label>
+        <input name="username" id="username" placeholder="AzureDiamond" value="" type="text"></input>
+      </div>
+      <br />
+      Password<br />
+      <div class="input-group">
+        <label for="password">🔑</label>
+        <input name="password" id="password" placeholder="hunter2" value="" type="password"></input>
+      </div>
+      <br />
+      <input type="submit" id="maximumGo" value="Log in"></input>
+    </form>
+</div>

+ 0 - 0
www/templates/midtitle.tx


+ 21 - 0
www/templates/notconfigured.tx

@@ -0,0 +1,21 @@
+<nav>
+    <strong>tCMS Initial Setup</strong>
+</nav>
+<section>
+    <div id="notice">
+        <img alt="Icon indicating a missing file" src="img/mime/missing.gif" />
+        <span>
+            [INFO] This page is being displayed because the main configuration file is missing
+            (or corrupted).
+        </span>
+    </div>
+    <p>
+        <strong>Note:</strong> Please see the
+        <a href="https://tcms.troglodyne.net/index.php?nav=5&post=fileshare/manual/Chapter%2000-Introduction.post">
+            tCMS Manual
+        </a>
+        for full instructions on how configure tCMS.
+        <br /><br />
+        Please <a href="sys/admin/config" alt="Login">Log In</a> and Configure tCMS.
+    </p>
+</section>

+ 18 - 0
www/templates/post.tx

@@ -0,0 +1,18 @@
+<div id="mbengine">
+ <div id="submissions">
+  <p class="title">Submissions:</p>
+  <form id="Submissions" method="POST">
+   Title *<br /><input class="cooltext" type="text" name="title" placeholder="Iowa Man Destroys Moon" />
+   URL *<br /><input class="cooltext" type="text" name="URL" placeholder="https://oneweirdtrick.scam" />
+   Image<br /><input class="cooltext" type="text" name="IMG" placeholder="https://gifdump.tld/Advice_Dog.jpg" />
+   Audio<br /><input class="cooltext" type="text" name="AUD" placeholder="https://soundclod.com/static.mp3"/>
+   Video<br /><input class="cooltext" type="text" name="VID" placeholder="https://youvimeo.tv/infomercial.mp4" />
+   Comments<br /><textarea class="cooltext" name="comment" placeholder="Potzrebie"></textarea>
+   <input type="hidden" name="app" value="microblog" />
+   <input class="coolbutton" type="submit" value="Publish" text="Publish" />
+  </form>
+ </div>
+ <div id="stories">
+   : include "posts.tx";
+ </div>
+</div>

+ 4 - 0
www/templates/posts.tx

@@ -0,0 +1,4 @@
+<p class="title"><a title="RSS" class="rss" href="/sys/rss/microblog.php"></a> Headline Nudes (<: $date :>):</p>
+: for $posts -> $post {
+:   $post.data | mark_raw
+: }

+ 1 - 0
www/templates/rightbar.tx

@@ -0,0 +1 @@
+POTZREBIE

+ 1 - 0
www/templates/title.tx

@@ -0,0 +1 @@
+tCMS

TEMPAT SAMPAH
www/themes/teodesian.net/img/icon/dave.png


TEMPAT SAMPAH
www/themes/teodesian.net/img/misc/linode.png


TEMPAT SAMPAH
www/themes/teodesian.net/img/sys/desian.png


TEMPAT SAMPAH
www/themes/teodesian.net/img/sys/logo.png


TEMPAT SAMPAH
www/themes/teodesian.net/img/sys/tsn.png


+ 25 - 0
www/themes/teodesian.net/routes.pm

@@ -0,0 +1,25 @@
+package Theme;
+
+use lib 'lib';
+use Trog::Routes::HTML;
+
+my %routes = {
+    '/links' => {
+        method   => 'GET',
+        callback => \&links,
+    },
+};
+
+my $processor = Text::Xslate->new(
+    path => 'www/themes/teodesian.net/templates',
+);
+
+sub links ($query, $input, $render_cb) {
+    my $content = $processor->render('links.tx', {
+        title => "Approved Propaganda from the ministry of family values",
+        theme_dir => 'www/themes/teodesian.net',
+    });
+    return Trog::Routes::HTML::index($query, $input, $render_cb, $content);
+}
+
+1;

+ 66 - 0
www/themes/teodesian.net/styles/screen.css

@@ -0,0 +1,66 @@
+body {
+    color: white;
+    background-image: url(/themes/teodesian.net/img/sys/tsn.png);
+    font-size: 150%;
+}
+
+#kontent {
+    background-color: rgba(0, 0, 0, 0.75);
+    padding: 2em 0.5em 0.5em;
+    border-radius: 0px 0px 1em 1em;
+    box-shadow: 0 0.5em 1em black;
+}
+
+a {
+    color: rgb(0,255,0);
+    border: 0px;
+}
+
+.LT_powered {
+    display: none;
+}
+
+.nowrap {
+    white-space:nowrap;
+}
+
+a.logo {
+    display: inline-block;
+    display: -webkit-flex;
+    display: -ms-flex;
+    display: flex;
+    margin-left: auto;
+    margin-right: auto;
+    height: 1.5em;
+    width: 10em;
+    background-image: url(/themes/teodesian.net/img/sys/logo.png);
+    background-size: auto 1.5em;
+    background-repeat: no-repeat;
+    background-position: center;
+    filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='themes/teodesian.net/img/sys/logo.png', sizingMethod='scale');
+    -ms-filter: "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='themes/teodesian.net/img/sys/logo.png', sizingMethod='scale')";
+}
+
+/*Responsive Shizz*/
+@media(max-width: 900px) {
+ body {
+  font-size: 100%;
+ }
+ #kontent {
+  max-width: 80vw;
+  padding: 3em 0.5em 0.5em;
+ }
+ #leftbar {
+  display: none;
+ }
+ img.nudes, .mblogimg {
+  max-width: 95vw;
+ }
+}
+/*Responsive iframes*/
+@media(max-width: 35rem) {
+  iframe.responsive, video.responsive {
+    width: 100%;
+    height: 10rem;
+  }
+}

+ 8 - 0
www/themes/teodesian.net/styles/structure.css

@@ -0,0 +1,8 @@
+#leftbar {
+    display: block;
+    padding-right: 0px;
+}
+
+#midtitle {
+    display: table-cell;
+}

+ 21 - 0
www/themes/teodesian.net/templates/leftbar.tx

@@ -0,0 +1,21 @@
+<p id="linkcontainer">
+    <span class="bold">BONUS:</span><br />
+    <a href="/links" title="Approved Propaganda from the Ministry of Family Values">Links</a><br />
+    <a href="irc://teodesian.net:6697/liberty" title="PROTIP: /connect -SSL irc.teodesian.net if you use IRSSI like a man">IRC</a><br />
+    <br />
+    <span class="bold">FRIENDS:</span><br />
+    <span class="nowrap">
+        <img alt="" class="icon" src="<: $theme_dir :>/img/icon/dave.png">
+        <a class="menu" target="_blank" href="http://thehhp.net" title="Y000da's Site">The HHP</a><br>
+    </span>
+    <br />
+    <span id="w6860b1ca7f3fde1e9592f57a17151a4c" class="libraryThing">
+    </span>
+    <script type="text/javascript" charset="UTF-8" src="https://www.librarything.com/widget_get.php?userid=teodesian&theID=w6860b1ca7f3fde1e9592f57a17151a4c"></script>
+    <noscript><a href="http://www.librarything.com/profile/teodesian">TEODESIAN.NET Library</a></noscript>
+    <br />
+    <a href="https://www.linode.com/?r=5af48069af206c2eafce639336dd589bf77739e5" title="1337 H4X0R Hosting">
+        <img style="height: 2em;" src="<: $theme_dir :>/img/misc/linode.png" alt="Linode">
+    </a>
+    <br />
+</p>

+ 168 - 0
www/themes/teodesian.net/templates/links.tx

@@ -0,0 +1,168 @@
+<p style="text-align: center; font-style: italic;">News Sources</p>
+  <span class="linkcontainer">
+   <a title="Slashdot: News for nerds. Stuff that matters" href="http://slashdot.org" target="_blank">
+    <img alt="Slashdot" border="0"  src="/links/img/slashdot.png" />
+   </a>
+   <a title="Hacker News" href="http://news.ycombinator.com" target="_blank">
+    <img alt="Hacker News" border="0"  src="/links/img/hn.png" />
+   </a>
+   <a title="Inside Automotive" href="http://insideautomotive.com" target="_blank">
+    <img alt="Ed Wallace's Inside Automotive" border="0"  src="/links/img/insideauto.png" />
+   </a>
+   <a title="Ludwig Von Mises Institute" href="http://mises.org" target="_blank">
+    <img alt="LvMI" border="0"  src="/links/img/mises.png" />
+   </a>
+   <a title="LewRockwell.com" href="http://www.lewrockwell.com" target="_blank">
+    <img alt="LewRockwell.com" border="0"  src="/links/img/LR.png" />
+   </a>
+   <a title="EconomicPolicyJournal" href="http://economicpolicyjournal.com" target="_blank">
+    <img alt="EPJ" border="0" src="/links/img/epj.png" target="_blank">
+   </a>
+    <a title="Zero Hedge" href="http://zerohedge.com" target="_blank">
+    <img alt="ZeroHedge" border="0" src="/links/img/zh.png" target="_blank">
+   </a>
+   <a title="Yves Smith's Naked Capitalism" href="http://www.nakedcapitalism.com" target="_blank">
+    <img alt="Econned?" border="0"  src="/links/img/econned.png" />
+   </a>
+   <a title="Freedom's Phoenix" href="http://freedomsphoenix.com" target="_blank">
+    <img alt="Freedom's Phoenix" border="0"  src="/links/img/ff.png" />
+   </a>
+   <a title="Anti-War.Com" href="http://antiwar.com" target="_blank">
+    <img alt="AntiWar.Com" border="0"  src="/links/img/awc.png" />
+   </a>
+   <a title="Boiling Frogs Post" href="http://boilingfrogspost.com" target="_blank">
+    <img alt="BFP" border="0"  src="/links/img/bfp.png" />
+   </a>
+   <a title="Voltaire Network" href="http://voltairenet.org" target="_blank">
+    <img alt="Voltaire" border="0"  src="/links/img/voltaire.png" />
+   </a>
+   <a title="Crypto-Gon" href="http://cryptogon.com" target="_blank">
+    <img alt="Cryptogon" border="0"  src="/links/img/cryptogon.png" />
+   </a>
+   <a title="IEEE Spectrum" href="http://spectrum.ieee.org" target="_blank">
+    <img alt="IEEE Spectrum" border="0"  src="/links/img/spectrum.png" />
+   </a>
+   <a title="Intel News" href="http://intelnews.org.org" target="_blank">
+    <img alt="Intel News" border="0"  src="/links/img/intelnews.png" />
+   </a>
+  </span>
+  <p style="text-align: center; font-style: italic;">Podcasts and Video</p>
+  <span class="linkcontainer">
+   <a title="LRC Podcasts" href="http://www.lewrockwell.com/lewrockwell-show/" target="_blank">
+    <img alt="LRC Podcasts" border="0"  src="/links/img/lrcpod.png" />
+   </a>
+   <a title="Anarchast" href="http://anarchast.com" target="_blank">
+    <img alt="Anarchast" border="0"  src="/links/img/anarchast.png" />
+   </a>
+   <a title="LRN.FM" href="http://www.lrn.fm" target="_blank">
+    <img alt="LRN.FM" border="0"  src="/links/img/lrn.png" />
+   </a>
+   <a title="SchiffRadio.com" href="http://www.schiffradio.com" target="_blank">
+    <img alt="SchiffRadio.com" border="0"  src="/links/img/schiffradio.png" />
+   </a>
+    <a title="Tom Woods" href="http://schiffradio.com/f/Tom-Woods" target="_blank">
+    <img alt="Tom Woods" border="0"  src="/links/img/tomwoods.png" />
+   </a>
+   <a title="The Corbett Report" href="http://corbettreport.com" target="_blank">
+    <img alt="Corbett Report" border="0"  src="/links/img/corbett.png" />
+   </a>
+   <a title="Declare your Independence" href="http://www.freedomsphoenix.com/RSS/Pod-Cast-Feed.xml" target="_blank">
+    <img alt="DYI-EH" border="0"  src="/links/img/ehancock.png" />
+   </a>
+   <a title="Scott Horton Show (Anti-War Radio)" href="http://scotthorton.org">
+    <img alt="Scott Horton" border="0" src="/links/img/antiWarRadio.png" />
+   </a>
+  </span>
+  <p style="text-align: center; font-style: italic;">Webcomics</p>
+  <span class="linkcontainer">
+   <a title="Dr. McNinja" href="http://www.drmcninja.com/" target="_blank">
+    <img alt="Dr. McNinja" border="0"  src="/links/img/mcninja.png" />
+   </a>
+   <a title="Saturday Morning Breakfast Cereal" href="http://www.smbc-comics.com" target="_blank">
+    <img alt="SMBC" border="0"  src="/links/img/smbc.png" />
+   </a>
+   <a title="Wigu" href="http://www.jjrowland.com" target="_blank">
+    <img alt="WIGU" border="0"  src="/links/img/wigu.png" />
+   </a>
+   <a title="VG Cats" href="http://www.vgcats.com" target="_blank">
+    <img alt="VG Cats" border="0"  src="/links/img/vgcats.png" />
+   </a>
+   <a title="Sluggy Freelance" href="http://www.sluggy.com" target="_blank">
+    <img alt="Sluggy Freelance" border="0"  src="/links/img/sluggy.png" />
+   </a>
+   <a title="Joe Loves Crappy Movies" href="http://www.digitalpimponline.com/strips.php?title=movie" target="_blank">
+    <img alt="JLCM" border="0"  src="/links/img/pimp.png" />
+   </a>
+   <a title="Penny Arcade" href="http://www.penny-arcade.com" target="_blank">
+    <img alt="Penny Arcade" border="0"  src="/links/img/pennyarcade.png" />
+   </a>
+   <a title="Diesel Sweeties" href="http://www.dieselsweeties.com/" target="_blank">
+    <img alt="Diesel Sweeties" border="0"  src="/links/img/sweeties.png" />
+   </a>
+   <a title="Rumblo" href="http://kcgreendotcom.com" target="_blank">
+    <img alt="Rumblo" border="0"  src="/links/img/rumblo.png" />
+   </a>
+   <a title="Scary Go Round" href="http://www.scarygoround.com" target="_blank">
+    <img alt="Scary Go Round" border="0"  src="/links/img/scarygoround.png" />
+   </a>
+  </span>
+  <p style="text-align: center; font-style: italic;">BONUS MATERIAL</p>
+  <span class="linkcontainer">
+   <a title="Fourmilab" href="http://fourmilab.ch" target="_blank">
+    <img alt="Fourmilab" border="0"  src="/links/img/fourmilab.png" />
+   </a>
+   <a title="BASH.ORG IRC Quote DB" href="http://www.bash.org" target="_blank">
+    <img alt="BASH.ORG" border="0"  src="/links/img/bash.png" />
+   </a>
+   <a title="SHOOP DA WOOP" href="http://www.encyclopediadramatica.wiki" target="_blank">
+    <img alt="8 [ o ]" border="0"  src="/links/img/shooplink.png" />
+   </a>
+   <a title="SPACE GHETTO" href="http://spaceghetto.net/sg" target="_blank">
+    <img alt="SG" border="0"  src="/links/img/sg.png" />
+   </a>
+  </span>
+  <p style="text-align: center; font-style: italic;">Hall of the Honored Dead</p>
+  <span class="linkcontainer">
+   <a title="Anarchy Gumbo" href="http://kittyfeet.com" target="_blank">
+    <img alt="KittyFeet" border="0"  src="/links/img/kittyfeet.png" />
+   </a>
+   <a title="Alien Loves Predator" href="http://www.alienlovespredator.com" target="_blank">
+    <img alt="Alien Loves Predator" border="0"  src="/links/img/alp.png" />
+   </a>
+   <a title="Bigger than Cheeses" href="http://www.biggercheese.com" target="_blank">
+    <img alt="Bigger than Cheeses" border="0"  src="/links/img/cheeses.png" />
+   </a>
+   <a title="Atland" href="http://www.realmofatland.com" target="_blank">
+    <img alt="Atland" border="0"  src="/links/img/atland.png" />
+   </a>
+   <a title="The Secret of Mana Theatre" href="http://www.manatheater.com" target="_blank">
+    <img alt="SoM Theatre" border="0"  src="/links/img/secretofmana.png" />
+   </a>
+   <a title="8 Bit Theatre" href="http://www.nuklearpower.com" target="_blank">
+    <img alt="8-Bit Theatre" border="0"  src="/links/img/nuk.png" />
+   </a>
+   <a title="Theatre Hopper" href="http://www.theaterhopper.com" target="_blank">
+    <img alt="Theatre Hopper" border="0"  src="/links/img/th.png" />
+   </a>
+   <a title="Rob and Elliot" href="http://www.robandelliot.cycomics.com/" target="_blank">
+    <img alt="Rob and Elliot" border="0"  src="/links/img/robandelliot.png" />
+   </a>
+   <a title="Texas Overnight" href="http://dfw.cbslocal.com/category/watch-listen/overnight-with-charley-jones/" target="_blank">
+    <img alt="Texas Overnight" border="0"  src="/links/img/charleyjones.png" />
+   </a>
+   <a title="The Wall St. Shuffle" href="http://thewallstreetshuffle.com" target="_blank">
+    <img alt="WSS" border="0"  src="/links/img/wss.png" />
+   </a>
+   <a title="Will Grigg" href="https://freedominourtime.blogspot.com/" target="_blank">
+    <img alt="FreedomZealot" border=0 src="/links/img/will_grigg.png" />
+   </a>
+   <a title="Mogambo Guru" href="http://mogamboguru.com" target="_blank">
+    <img alt="WFD!!!" border="0"  src="/links/img/mogambo.png" />
+   </a>
+   <a title="Freedom Feens" href="http://freedomfeens.com" target="_blank">
+    <img alt="FEENS" border="0"  src="/links/img/feens.png" />
+   </a>
+   <a title="Bad Quaker" href="http://badquaker.com" target="_blank">
+    <img alt="Bad Quaker" border="0"  src="/links/img/bq.png" />
+   </a>
+  </span>

+ 1 - 0
www/themes/teodesian.net/templates/midtitle.tx

@@ -0,0 +1 @@
+<a title="Your Brain is now Liquid" href="https://www.youtube.com/watch?v=8VBFeAESYeM" class="logo"></a>

+ 1 - 0
www/themes/teodesian.net/templates/title.tx

@@ -0,0 +1 @@
+<img id="D" src="<: $theme_dir :>/img/sys/desian.png" title="The D on my Grave stands for... DRACULA!" class="titlebar" />

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini