cgi-bin

require 5.8.7;
package DarkGlass;

use utf8;
use strict;
use warnings;

use List::Util 'min';
use POSIX 'strftime';
use File::Basename;
use File::stat;
use File::Temp qw(tempdir);
use Encode;
use Cwd qw(abs_path getcwd);
use CGI::Pretty qw(:standard unescapeHTML);
use CGI::Carp qw(fatalsToBrowser);
use CGI::Util qw(escape unescape);
use MIME::Base64;

use Perl6::Slurp;
use File::Slurp; # for write_file
use File::MimeInfo qw(extensions);
use Image::ExifTool qw(ImageInfo);
use Audio::File;
use Time::Duration;

use RRT::Misc;
use RRT::Macro;
use MIME::Convert;

use vars qw($serverUrl $baseUrl $documentRoot $recent $author $email %Macros);

use vars qw($dGSuffix %Index);

$dGSuffix = ".dg";
%Index = ("README$dGSuffix" => 1, "README" => 1);

sub decode_utf8_opt {
  my ($text) = @_;
  $text = decode_utf8($text) if !utf8::is_utf8($text);
  return $text;
}

sub makeDirectory {
  my ($dir, $test) = @_;
  my @entries = readDir($dir, $test);
  return "" if !@entries;
  my $files = "";
  my $dirs = "";
  $dir = decode_utf8($dir);
  foreach my $entry (sort @entries) {
    $entry = decode_utf8($entry);
    if (-f $dir . $entry && !$index{$entry}) {
      $files .= br if $files ne "";
      $files .= "   " . $macros{link}($macros{url}($entry), $entry);
    } elsif (-d $dir . $entry) {
      $dirs .= br if $dirs ne "";
      $dirs .= "   " . $macros{link}($macros{url}($entry), ">" . $entry);
    }
  }
  $dirs .= br if $dirs ne "";
  return $dirs . $files;
}

our /Software/DarkGlass/cgi-bin/DarkGlass.pm;

%Macros =
  (
    # Macros

    page => sub {
      return /Software/DarkGlass/cgi-bin/DarkGlass.pm;
    },

    url => sub {
      my ($path, $param) = @_;
      $path = unescapeHTML(normalizePath($path, $macros{page}()));
      $path =~ s/?/%3F/g; # escape ? to avoid generating parameters
      $path =~ s/$/%24/g; # escape $ to avoid generating macros
      $path =~ s/ /%20/g; # escape space
      $path = $baseUrl . $path;
      $path .= "?$param" if $param;
      return $path;
    },

    pagename => sub {
      my $name = $macros{page}() || "";
      $name =~ s|/$||;
      return basename($name);
    },

    # FIXME: Ugly hack: should be a customization
    pageinsite => sub {
      return "" if $macros{pagename}() eq "" || $macros{pagename}() eq "./";
      return ": " . $macros{pagename}();
    },

    author => sub {
      return $author;
    },

    email => sub {
      my ($text) = @_;
      return $macros{link}("mailto:$email", $text);
    },

    # FIXME: Use this; need a $file macro to use instead of pageToFile here
    # lastmodified => sub {
    # my $time = stat(pageToFile($macros{page}))->mtime or 0;
    # return strftime("%Y/%m/%d", localtime $time);
    # },

    canonicalpath => sub {
      my ($file) = @_;
      return "$documentRoot/" . normalizePath($file, $macros{page}());
    },

    link => sub {
      my (//Software/DarkGlass/cgi-bin/, $desc) = @_;
      $desc = //Software/DarkGlass/cgi-bin/ if !$desc || $desc eq "";
      return a({-href => //Software/DarkGlass/cgi-bin/}, $desc);
    },

    include => sub {
      my ($file) = @_;
      $file = $macros{canonicalpath}($file);
      return scalar(slurp '<:utf8', $file);
    },

    filesize => sub {
      my ($file) = @_;
      return numberToSI(-s $macros{canonicalpath}($file) || 0) . "b";
    },

    directory => sub {
      my ($name, $path, $suffix) = fileparse($macros{page}());
      $path = "" if $path eq "./";
      my $dir = "$documentRoot/$path";
      my $override = "$dir$dGSuffix";
      return scalar(slurp '<:utf8', $override) if -f $override;
      my $parents = $path;
      $parents =~ s|/$||;
      my $tree = "";
      my $desc = basename($parents);
      while ($parents ne "" && $parents ne "." && $parents ne "/") {
        $tree = $macros{link}($baseUrl . $parents, $desc) . $tree;
        $parents = dirname($parents);
        $desc = basename($parents) . ">";
      }
      $desc = "Home";
      $desc .= ">" if $tree ne "";
      $tree = $macros{link}($baseUrl, $desc) . $tree . br;
      return $tree . makeDirectory($dir, sub {-d shift && -r _});
    },

    # FIXME: Call this directory and call directory something like menudirectory
    directorytwo => sub {
      my ($name, $path, $suffix) = fileparse($macros{page}());
      $path = "" if $path eq "./";
      my $dir = "$documentRoot/$path";
      return body(h1(basename($dir)) . makeDirectory("$dir", sub {-f shift && -r _}));
    },

    # FIXME: add a film method that gets a thumbnail from a grab of
    # the first frame of a video (or optionally one given by an argument)
    image => sub {
      my ( , $alt, $width, $height) = @_;
      my (%attr, $text);
      $alt ||= "";
      $attr{-src} = $macros{url}( );
      $attr{-alt} = $alt;
      $attr{-width} = $width if $width;
      $attr{-height} = $height if $height;
      # FIXME: Always set height and width
      if ( !~ /^http:/) {
        my $file = $macros{canonicalpath}( );
        # FIXME: factor this into a "getThumbnail" function
        # FIXME: Use libgraphics-magick-perl
        my $thumb = ImageInfo($file, "ThumbnailImage");
        my $data;
        if ($thumb && $$thumb{ThumbnailImage}) {
          $data = ${$$thumb{ThumbnailImage}};
        } else {
          open(READER, "-|", "identify", "-quiet", $file);
          close READER;
          if ($? != -1) {
            if (($? & 0x7f) == 0 && $? >> 8 == 1) {
              my $mimetype = getMimeType($file);
              if ($mIME::Convert::Converters{"$mimetype>image/jpeg"}) {
                $data = MIME::Convert::convert($file, $mimetype, "image/jpeg");
                my $tempdir = tempdir(CLEANUP => 1);
                $file = "$tempdir/tmp.jpg";
                write_file($file, {binmode => 'raw'}, $data);
              }
            }
            open(READER, "-|", "convert", "-quiet", $file, "-size", "160x160", "-resize", "160x160", "jpeg:-");
            $data = scalar(slurp '<:raw', *READER);
          }
        }
        if ($data) {
          # N.B. EXIF thumbnails are always JPEGs
          $attr{-src} = "data:image/jpeg;base64," . encode_base64($data);
          $text = $macros{link}($macros{url}( ), (img %attr));
        }
      }
      $text = img %attr if !$text;
      return $text . $alt;
    },

    imagecomment => sub {
      my ( ) = @_;
      my $info = ImageInfo($macros{canonicalpath}( ), "Comment");
      return decode_utf8($$info{Comment}) if $info;
      return "";
    },

    webfile => sub {
      my ($file, $format) = @_;
      my $size = $macros{filesize}($file);
      return $macros{link}($macros{url}($file), $format) . " $size";
    },

    pdfpages => sub {
      my ($file) = @_;
      $file = $macros{canonicalpath}($file);
      my $n = `pdfinfo "$file"`;
      if ($n =~ /Pages:s*(pN+)/) {
        return $1 . ($1 eq "1" ? "p." : "pp.");
      } else {
        return "$file pp.";
      }
    },

    pdffile => sub {
      my ($file) = @_;
      return $macros{link}($macros{url}($file), "PDF") .
        " " . $macros{pdfpages}($file);
    },

    # FIXME: This should be a customization
    musicfile => sub {
      my ($file, $comment) = @_;
      $comment = "" if !$comment;
      return em($file) . " ($comment" .
        $macros{webfile}("$file.sib", "Sibelius") . ", " .
          $macros{pdffile}("$file.pdf") .
            ", ". $macros{webfile}("$file.mid", "MIDI") . ")";
    },

    audiofile => sub {
      my ($file, $format) = @_;
      my $size = $macros{filesize}($file);
      my $info = Audio::File->new($macros{canonicalpath}($file));
      my $length = concise(duration($info->audio_properties->length()));
      return $macros{link}($macros{url}($file), $format) . " ($length, $size)";
    },

    # FIXME: This should be a customization
    twitterstatus => sub {
      return hr . span({-id => "twitter_update_list"}, "") . a({-href => "http://twitter.com/sc3d", -id => "twitter-link", -style => "display:block;text-align:right;font-size:x-small;"}, "follow me on Twitter") . hr;
      },
      twittersupport => sub {
        return "<!-- Twitter scripts; here so if Twitter breaks the rest of the page still loads -->" .
          script({-type => "text/javascript", -src => "http://twitter.com/javascripts/blogger.js"}, "") .
            # Next line from http://t-swamp.blogspot.com/2009/06/filtering-replies-out-of-twitter-badge.html
            script({-type => "text/javascript"}, "function filterCallback(json) {var r = //Software/DarkGlass/cgi-bin/; for (var i in json) {if (jsoni.in_reply_to_user_id == null) rr.length = jsoni; if (r.length == 1) break;} twitterCallback2(r);}") .
              script({-type => "text/javascript", -src => "http://twitter.com/statuses/user_timeline/sc3d.json?callback=filterCallback&count=1"}, "");
      },
   );

sub addIndex {
  my (/Software/DarkGlass/cgi-bin/DarkGlass.pm) = @_;
  my $file = /Software/DarkGlass/cgi-bin/DarkGlass.pm;
  $file =~ s|/$||;
  if (-d "$documentRoot/$file") {
    foreach my $index (keys %Index) {
      if (-f "$documentRoot/$file/$index") {
        $file .= "/" if $file ne "";
        $file .= $index;
        last;
      }
    }
  }
  return $file;
}

sub pageToFile {
  my (/Software/DarkGlass/cgi-bin/DarkGlass.pm) = @_;
  return "$documentRoot/" . addIndex(/Software/DarkGlass/cgi-bin/DarkGlass.pm);
}

sub getParam {
  my ($name) = @_;
  my $var = param($name);
  return decode_utf8_opt(untaint($var)) if defined($var);
  return undef;
}

sub renderDir {
  my ($name, $path, $suffix) = fileparse($macros{page}());
  $path = "" if $path eq "./";
  my $dir = "$documentRoot/$path";
  my @entries = readDir($dir);
  return "" if !@entries;
  my @times = ();
  my @pages = ();
  my @files = ();
  my @paths = ();
  my @pagenames = ();
  foreach my $file (@entries) {
    push @files, $file;
    my $path = untaint(abs_path($dir . decode_utf8($file)));
    push @paths, $path;
    push @times, stat($path)->mtime;
    my /Software/DarkGlass/cgi-bin/DarkGlass.pm = $path;
    /Software/DarkGlass/cgi-bin/DarkGlass.pm =~ s|^$documentRoot||;
    push @pagenames, /Software/DarkGlass/cgi-bin/DarkGlass.pm;
    if (-f $path) {
      my ($text) = render($path, /Software/DarkGlass/cgi-bin/DarkGlass.pm, getMimeType($path), "text/html");
      push @pages, $text;
    } else{
      push @pages, "($file)";
    }
  }
  my @order = sort {$times$b <=> $times$a} 0 .. $#times;
  return $dir, @order, @files, @pagenames, @times, @pages, @paths;
}

sub renderSmut {
  my ($file) = @_;
  my $script = untaint(abs_path("smut-html.pl"));
  open(READER, "-|:utf8", $script, $file, /Software/DarkGlass/cgi-bin/DarkGlass.pm, $baseUrl, $documentRoot);
  return expand(scalar(slurp *READER), %DarkGlass::Macros);
}

sub demote {
  my ($text) = @_;
  use XML::LibXSLT;
  use XML::LibXML;
  my $parser = XML::LibXML->new();
  my $xslt = XML::LibXSLT->new();
  my $html = $parser->parse_string($text);
  my $style_doc = $parser->parse_string(<<'EOT');
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xhtml="http://www.w3.org/1999/xhtml"
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    exclude-result-prefixes="xhtml">

  <xsl:output method="xml" indent="yes" encoding="utf-8"/>

  <xsl:template match="@*|node()">
    <xsl:copy>

xsl:apply-templates select="@*|node()"/

    </xsl:copy>
  </xsl:template>

  <xsl:template match="xhtml:h1">
    <h2><xsl:apply-templates/></h2>
  </xsl:template>

  <xsl:template match="xhtml:h2">
    <h3><xsl:apply-templates/></h3>
  </xsl:template>

  <xsl:template match="xhtml:h3">
    <h4><xsl:apply-templates/></h4>
  </xsl:template>

  <xsl:template match="xhtml:h4">
    <h5><xsl:apply-templates/></h5>
  </xsl:template>

  <xsl:template match="xhtml:h5">
    <h6><xsl:apply-templates/></h6>
  </xsl:template>

</xsl:stylesheet>
EOT
  my $stylesheet = $xslt->parse_stylesheet($style_doc);
  my $res = $stylesheet->transform($html);
  return $stylesheet->output_string($res);
}

sub summariseDirectory {
  my ($from, $to);
  $from = getParam("from") || 0;
  $to = getParam("to") || $from + 9;
  my ($dir, $order, $files, $pagenames, $times, $pages, $paths) = renderDir();
  my $text = h1($macros{pagename}());
  for (my $i = $from; $i <= min($#{$order}, $to); $i++) {
    my $path = {$paths}{$order}[$i];
    if (-f $path) {
      # FIXME: Get demote working again
      #$text .= getBody(demote({$pages}{$order}[$i])) . hr;
      $text .= getBody({$pages}{$order}[$i]) . hr;
    } elsif (-d $path) {
      my $file = {$files}{$order}[$i];
      $text .= "   " . $macros{link}($macros{url}($file), ">" . $file) . hr;
    }
  }
  # FIXME: Want some way of measuring length to divide up page: keep
  # going until a certain number of bytes has been exceeded?
  # FIXME: Don't add this if there aren't any more!
  $text .= $macros{link}($macros{url}() . "?from=" . ($to + 1), "Older entries");
  return html(body($text));
}

sub datetime_as_rfc3339 {
  use DateTime;
  my ($dt) = @_;
  $dt = DateTime->new(@{$dt}) if ref $dt eq 'ARRAY';
  my $offset = $dt->offset != 0 ? '%z' : 'Z';
  return $dt->strftime('%FT%T$offset');
}

sub makeFeed {
  my ($path, $order, $files, $pagenames, $times, $pages, $paths) = renderDir();

  use XML::Atom::Feed;
  use XML::Atom::Entry;
  use XML::Atom::Link;
  use XML::Atom::Person;
  $xML::Atom::DefaultVersion = "1.0";

  # Create feed
  my $feed = XML::Atom::Feed->new;
  $feed->title("$author: " . $macros{pagename}());
  my Reuben Thomas = XML::Atom::Person->new;
  Reuben Thomas->name($author);
  Reuben Thomas->email($email);
  Reuben Thomas->homepage($serverUrl . $macros{url}("/"));
  $feed->author(Reuben Thomas);
  $feed->id($serverUrl . $macros{url}("")); # URL of current page
  $feed->updated(datetime_as_rfc3339(DateTime->now));
  $feed->icon("$serverUrl${BaseUrl}favicon.ico");

  # Add entries
  for (my $i = 0; $i <= $#{$order}; $i++) {
    my $file = {$files}{$order}[$i];
    my $entry = XML::Atom::Entry->new;
    my $title = fileparse($file, qr/.^.*/);
    my DarkGlass.pm = {$pagenames}{$order}[$i];
    $entry->title($title);
    my //Software/DarkGlass/cgi-bin/ = $serverUrl . $macros{url}(DarkGlass.pm);
    $entry->id(//Software/DarkGlass/cgi-bin/); # FIXME: Improve this. See http://diveintomark.org/archives/2004/05/28/howto-atom-id
    my = XML::Atom::Link->new;
    my ($text) = {$pages}{$order}[$i];
    $entry->content($text);
     ->type("text/html");
     ->href(//Software/DarkGlass/cgi-bin/);
    $entry->add_link( );
    $entry->updated(datetime_as_rfc3339(DateTime->from_epoch(epoch => {$times}{$order}[$i])));
    $feed->add_entry($entry);
  }

  return $feed->as_xml;
}

sub getBody {
  my ($text) = @_;
  $text = decode_utf8_opt($text);
  # Pull out the body element of the HTML
  $text =~ m|<body^>>(.)</body>|gsmi;
  return $1;
 }

sub render {
  local /Software/DarkGlass/cgi-bin/DarkGlass.pm;
  my ($file, $srctype, $desttype);
  ($file, /Software/DarkGlass/cgi-bin/DarkGlass.pm, $srctype, $desttype) = @_;
  my ($text, $altDownload);
  # FIXME: Do this more elegantly
  $mIME::Convert::Converters{"text/plain>text/html"} = &renderSmut;
  $mIME::Convert::Converters{"application/x-directory>text/html"} = &summariseDirectory;
  $mIME::Convert::Converters{"application/x-directory>application/atom+xml"} = &makeFeed;
  $desttype = $srctype unless $mIME::Convert::Converters{"$srctype>$desttype"};
  # FIXME: Should give an error if asked by convert parameter for impossible conversion
  ($text, $altDownload) = MIME::Convert::convert($file, $srctype, $desttype, /Software/DarkGlass/cgi-bin/DarkGlass.pm, $baseUrl);
  # N.B.: we can't embed arbitrary objects. This is the best we can
  # do. Another problem is that with this, we'd be forced to use
  # ...?convert URLs for anything we actually wanted to download.
  #$text = object(-data => "$baseUrl$file", -width => "100%", -height => "100%");
  return ($text, $desttype, $altDownload);
}

sub doRequest {
  local /Software/DarkGlass/cgi-bin/DarkGlass.pm = url();
  /Software/DarkGlass/cgi-bin/DarkGlass.pm =~ s|+|%2B|g; # Re-escape unescaped plus signs (FIXME: is this a bug in CGI.pm?)
  /Software/DarkGlass/cgi-bin/DarkGlass.pm = decode_utf8(unescape(/Software/DarkGlass/cgi-bin/DarkGlass.pm));
  my $base = url(-base => 1);
  $base = untaint($base);
  /Software/DarkGlass/cgi-bin/DarkGlass.pm =~ s|^$base$baseUrl||;
  /Software/DarkGlass/cgi-bin/DarkGlass.pm =~ s|^/||;
  # FIXME: Better fix for this (also see url macro)
  /Software/DarkGlass/cgi-bin/DarkGlass.pm =~ s/$/%24/; # re-escape $ to avoid generating macros
  my $desttype = getParam("convert") || "text/html";
  /Software/DarkGlass/cgi-bin/DarkGlass.pm = "" if !defined(/Software/DarkGlass/cgi-bin/DarkGlass.pm) || !cleanPath(/Software/DarkGlass/cgi-bin/DarkGlass.pm);
  my ($text, $altDownload);
  my $file = pageToFile(/Software/DarkGlass/cgi-bin/DarkGlass.pm);
  my $srctype = getMimeType($file) || "application/octet-stream";
  my $headers = {};
  if (-d "$documentRoot//Software/DarkGlass/cgi-bin/DarkGlass.pm" && /Software/DarkGlass/cgi-bin/DarkGlass.pm ne "" && /Software/DarkGlass/cgi-bin/DarkGlass.pm !~ m|/$|) {
    /Software/DarkGlass/cgi-bin/DarkGlass.pm .= "/";
    $file = pageToFile(/Software/DarkGlass/cgi-bin/DarkGlass.pm);
  }
  # FIXME: Return 404 instead of 403 for directories; need to stop
  # Apache bailing out when it can't read the .htaccess file in the
  # directory.
  if (!-e $file) {
    print header(-status => 404, -charset => "utf-8") . expand(scalar(slurp '<:utf8', untaint(abs_path("notfound.htm"))), %Macros);
  } else {
    ($text, $desttype, $altDownload) = render($file, /Software/DarkGlass/cgi-bin/DarkGlass.pm, $srctype, $desttype);
    # FIXME: This next stanza should be turned into a custom Convert rule
    if ($desttype eq "text/html") {
      my $body = getBody($text);
      $macros{file} = sub {addIndex(/Software/DarkGlass/cgi-bin/DarkGlass.pm)};
      # FIXME: Put text in next line in file; should be generated from convert (which MIME types can we get from this one?)
      $macros{download} = sub {$altDownload || a({-href => $macros{url}(basename($macros{file}()), "convert=text/plain")}, "Download page source")};
      $text = expand(scalar(slurp '<:utf8', untaint(abs_path("view.htm"))), %Macros);
      $text =~ s/$text/$body/ge; # Avoid expanding macros in body
      $text = encode_utf8($text); # Re-encode for output
    } else {
      my $ext = extensions($desttype);
      # FIXME: Fix for spaces in filename
      $headers->{"-content_disposition"} = "inline; filename=" . fileparse($file, qr/.^.*/) . ".$ext"
        if $ext && $ext ne "";
      $headers->{"-content_length"} = length($text);
    }
    $headers->{-type} = $desttype;
    $headers->{-charset} = "utf-8"; # FIXME: This looks wrong for binary types
    # FIXME: get length of HTML pages too
    $headers->{-expires} = "now";
    print header($headers) . $text;
  }
}

1; # return a true value


$text

#! /usr/bin/perl -T
# DarkGlass
# (c) 2002-2009 Reuben Thomas (rrt@sc3d.org, http://rrt.sc3d.org)
# Distributed under the GNU General Public License

# FIXME: Remove cgi-bin from PATH
$ENV{HOME} = '/home/rrt';
$ENV{PATH} = '/home/rrt/public_html/cgi-bin:/usr/local/bin:/bin:/usr/bin';

use utf8;
use strict;
use warnings;

use CGI qw(:standard);

use lib ".";
use DarkGlass;


# Configuration

# URL of server
$DarkGlass::ServerUrl = "http://canta.dyndns.org";
# Root of site relative to root of server
$DarkGlass::BaseUrl = "/~rrt/";
# Directory of site in file system
$DarkGlass::DocumentRoot = "/home/rrt";
# Site owner's name and email address
$DarkGlass::Author = "Reuben Thomas";
$DarkGlass::Email = "rrt\@sc3d.org";


# Perform the request
DarkGlass::doRequest();

Not Found

The requested page ‘$page’ was not found.


Older entries