[Bps-public-commit] rt-authen-token branch, master, created. 3354110637f991ce4c8f7a1173ffb5595c65841a

Shawn Moore shawn at bestpractical.com
Thu Jul 6 18:23:43 EDT 2017


The branch, master has been created
        at  3354110637f991ce4c8f7a1173ffb5595c65841a (commit)

- Log -----------------------------------------------------------------
commit 6da5d67c8efc2c9d7556cdc7aafded75c2cd28d9
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 14:35:43 2017 +0000

    Scaffolding

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f8a48f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+blib*
+Makefile
+Makefile.old
+pm_to_blib*
+*.tar.gz
+.lwpcookies
+cover_db
+pod2htm*.tmp
+/RT-Authen-Token*
+*.bak
+*.swp
+/MYMETA.*
+/t/tmp
+/xt/tmp
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..7a4f33f
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,19 @@
+inc/Module/Install.pm
+inc/Module/Install/Base.pm
+inc/Module/Install/Can.pm
+inc/Module/Install/Fetch.pm
+inc/Module/Install/Include.pm
+inc/Module/Install/Makefile.pm
+inc/Module/Install/Metadata.pm
+inc/Module/Install/ReadmeFromPod.pm
+inc/Module/Install/RTx.pm
+inc/Module/Install/RTx/Runtime.pm
+inc/Module/Install/Win32.pm
+inc/Module/Install/WriteAll.pm
+inc/unicore/Name.pm
+inc/YAML/Tiny.pm
+lib/RT/Authen/Token.pm
+Makefile.PL
+MANIFEST			This list of files
+META.yml
+README
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..38e7f3f
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,27 @@
+---
+abstract: 'RT-Authen-Token Extension'
+author:
+  - 'Best Practical Solutions, LLC <modules at bestpractical.com>'
+build_requires:
+  ExtUtils::MakeMaker: 6.59
+configure_requires:
+  ExtUtils::MakeMaker: 6.59
+distribution_type: module
+dynamic_config: 1
+generated_by: 'Module::Install version 1.06'
+license: gpl
+meta-spec:
+  url: http://module-build.sourceforge.net/META-spec-v1.4.html
+  version: 1.4
+name: RT-Authen-Token
+no_index:
+  directory:
+    - inc
+requires:
+  perl: 5.8.3
+resources:
+  license: http://opensource.org/licenses/gpl-license.php
+version: 0.01
+x_module_install_rtx_version: 0.39
+x_requires_rt: 4.2
+x_rt_too_new: 4.6
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..022f658
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,10 @@
+use inc::Module::Install;
+
+RTx 'RT-Authen-Token';
+
+requires_rt '4.2';
+rt_too_new '4.6';
+
+WriteAll;
+sign;
+
diff --git a/README b/README
new file mode 100644
index 0000000..a4a19c0
--- /dev/null
+++ b/README
@@ -0,0 +1,37 @@
+NAME
+    RT-Authen-Token - token-based authentication
+
+INSTALLATION
+    RT-Authen-Token requires version RT 4.2.0 or later.
+
+    perl Makefile.PL
+    make
+    make install
+        This step may require root permissions.
+
+    Edit your /opt/rt4/etc/RT_SiteConfig.pm
+        Add this line:
+
+            Plugin( "RT::Authen::Token" );
+
+    Restart your webserver
+
+AUTHOR
+    Best Practical Solutions, LLC <modules at bestpractical.com>
+
+BUGS
+    All bugs should be reported via email to
+
+        L<bug-RT-Authen-Token at rt.cpan.org|mailto:bug-RT-Authen-Token at rt.cpan.org>
+
+    or via the web at
+
+        L<rt.cpan.org|http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Authen-Token>.
+
+COPYRIGHT
+    This extension is Copyright (C) 2017 Best Practical Solutions, LLC.
+
+    This is free software, licensed under:
+
+      The GNU General Public License, Version 2, June 1991
+
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
new file mode 100644
index 0000000..4ecf46b
--- /dev/null
+++ b/inc/Module/Install.pm
@@ -0,0 +1,470 @@
+#line 1
+package Module::Install;
+
+# For any maintainers:
+# The load order for Module::Install is a bit magic.
+# It goes something like this...
+#
+# IF ( host has Module::Install installed, creating author mode ) {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to installed version of inc::Module::Install
+#     3. The installed version of inc::Module::Install loads
+#     4. inc::Module::Install calls "require Module::Install"
+#     5. The ./inc/ version of Module::Install loads
+# } ELSE {
+#     1. Makefile.PL calls "use inc::Module::Install"
+#     2. $INC{inc/Module/Install.pm} set to ./inc/ version of Module::Install
+#     3. The ./inc/ version of Module::Install loads
+# }
+
+use 5.005;
+use strict 'vars';
+use Cwd        ();
+use File::Find ();
+use File::Path ();
+
+use vars qw{$VERSION $MAIN};
+BEGIN {
+	# All Module::Install core packages now require synchronised versions.
+	# This will be used to ensure we don't accidentally load old or
+	# different versions of modules.
+	# This is not enforced yet, but will be some time in the next few
+	# releases once we can make sure it won't clash with custom
+	# Module::Install extensions.
+	$VERSION = '1.06';
+
+	# Storage for the pseudo-singleton
+	$MAIN    = undef;
+
+	*inc::Module::Install::VERSION = *VERSION;
+	@inc::Module::Install::ISA     = __PACKAGE__;
+
+}
+
+sub import {
+	my $class = shift;
+	my $self  = $class->new(@_);
+	my $who   = $self->_caller;
+
+	#-------------------------------------------------------------
+	# all of the following checks should be included in import(),
+	# to allow "eval 'require Module::Install; 1' to test
+	# installation of Module::Install. (RT #51267)
+	#-------------------------------------------------------------
+
+	# Whether or not inc::Module::Install is actually loaded, the
+	# $INC{inc/Module/Install.pm} is what will still get set as long as
+	# the caller loaded module this in the documented manner.
+	# If not set, the caller may NOT have loaded the bundled version, and thus
+	# they may not have a MI version that works with the Makefile.PL. This would
+	# result in false errors or unexpected behaviour. And we don't want that.
+	my $file = join( '/', 'inc', split /::/, __PACKAGE__ ) . '.pm';
+	unless ( $INC{$file} ) { die <<"END_DIE" }
+
+Please invoke ${\__PACKAGE__} with:
+
+	use inc::${\__PACKAGE__};
+
+not:
+
+	use ${\__PACKAGE__};
+
+END_DIE
+
+	# This reportedly fixes a rare Win32 UTC file time issue, but
+	# as this is a non-cross-platform XS module not in the core,
+	# we shouldn't really depend on it. See RT #24194 for detail.
+	# (Also, this module only supports Perl 5.6 and above).
+	eval "use Win32::UTCFileTime" if $^O eq 'MSWin32' && $] >= 5.006;
+
+	# If the script that is loading Module::Install is from the future,
+	# then make will detect this and cause it to re-run over and over
+	# again. This is bad. Rather than taking action to touch it (which
+	# is unreliable on some platforms and requires write permissions)
+	# for now we should catch this and refuse to run.
+	if ( -f $0 ) {
+		my $s = (stat($0))[9];
+
+		# If the modification time is only slightly in the future,
+		# sleep briefly to remove the problem.
+		my $a = $s - time;
+		if ( $a > 0 and $a < 5 ) { sleep 5 }
+
+		# Too far in the future, throw an error.
+		my $t = time;
+		if ( $s > $t ) { die <<"END_DIE" }
+
+Your installer $0 has a modification time in the future ($s > $t).
+
+This is known to create infinite loops in make.
+
+Please correct this, then run $0 again.
+
+END_DIE
+	}
+
+
+	# Build.PL was formerly supported, but no longer is due to excessive
+	# difficulty in implementing every single feature twice.
+	if ( $0 =~ /Build.PL$/i ) { die <<"END_DIE" }
+
+Module::Install no longer supports Build.PL.
+
+It was impossible to maintain duel backends, and has been deprecated.
+
+Please remove all Build.PL files and only use the Makefile.PL installer.
+
+END_DIE
+
+	#-------------------------------------------------------------
+
+	# To save some more typing in Module::Install installers, every...
+	# use inc::Module::Install
+	# ...also acts as an implicit use strict.
+	$^H |= strict::bits(qw(refs subs vars));
+
+	#-------------------------------------------------------------
+
+	unless ( -f $self->{file} ) {
+		foreach my $key (keys %INC) {
+			delete $INC{$key} if $key =~ /Module\/Install/;
+		}
+
+		local $^W;
+		require "$self->{path}/$self->{dispatch}.pm";
+		File::Path::mkpath("$self->{prefix}/$self->{author}");
+		$self->{admin} = "$self->{name}::$self->{dispatch}"->new( _top => $self );
+		$self->{admin}->init;
+		@_ = ($class, _self => $self);
+		goto &{"$self->{name}::import"};
+	}
+
+	local $^W;
+	*{"${who}::AUTOLOAD"} = $self->autoload;
+	$self->preload;
+
+	# Unregister loader and worker packages so subdirs can use them again
+	delete $INC{'inc/Module/Install.pm'};
+	delete $INC{'Module/Install.pm'};
+
+	# Save to the singleton
+	$MAIN = $self;
+
+	return 1;
+}
+
+sub autoload {
+	my $self = shift;
+	my $who  = $self->_caller;
+	my $cwd  = Cwd::cwd();
+	my $sym  = "${who}::AUTOLOAD";
+	$sym->{$cwd} = sub {
+		my $pwd = Cwd::cwd();
+		if ( my $code = $sym->{$pwd} ) {
+			# Delegate back to parent dirs
+			goto &$code unless $cwd eq $pwd;
+		}
+		unless ($$sym =~ s/([^:]+)$//) {
+			# XXX: it looks like we can't retrieve the missing function
+			# via $$sym (usually $main::AUTOLOAD) in this case.
+			# I'm still wondering if we should slurp Makefile.PL to
+			# get some context or not ...
+			my ($package, $file, $line) = caller;
+			die <<"EOT";
+Unknown function is found at $file line $line.
+Execution of $file aborted due to runtime errors.
+
+If you're a contributor to a project, you may need to install
+some Module::Install extensions from CPAN (or other repository).
+If you're a user of a module, please contact the author.
+EOT
+		}
+		my $method = $1;
+		if ( uc($method) eq $method ) {
+			# Do nothing
+			return;
+		} elsif ( $method =~ /^_/ and $self->can($method) ) {
+			# Dispatch to the root M:I class
+			return $self->$method(@_);
+		}
+
+		# Dispatch to the appropriate plugin
+		unshift @_, ( $self, $1 );
+		goto &{$self->can('call')};
+	};
+}
+
+sub preload {
+	my $self = shift;
+	unless ( $self->{extensions} ) {
+		$self->load_extensions(
+			"$self->{prefix}/$self->{path}", $self
+		);
+	}
+
+	my @exts = @{$self->{extensions}};
+	unless ( @exts ) {
+		@exts = $self->{admin}->load_all_extensions;
+	}
+
+	my %seen;
+	foreach my $obj ( @exts ) {
+		while (my ($method, $glob) = each %{ref($obj) . '::'}) {
+			next unless $obj->can($method);
+			next if $method =~ /^_/;
+			next if $method eq uc($method);
+			$seen{$method}++;
+		}
+	}
+
+	my $who = $self->_caller;
+	foreach my $name ( sort keys %seen ) {
+		local $^W;
+		*{"${who}::$name"} = sub {
+			${"${who}::AUTOLOAD"} = "${who}::$name";
+			goto &{"${who}::AUTOLOAD"};
+		};
+	}
+}
+
+sub new {
+	my ($class, %args) = @_;
+
+	delete $INC{'FindBin.pm'};
+	{
+		# to suppress the redefine warning
+		local $SIG{__WARN__} = sub {};
+		require FindBin;
+	}
+
+	# ignore the prefix on extension modules built from top level.
+	my $base_path = Cwd::abs_path($FindBin::Bin);
+	unless ( Cwd::abs_path(Cwd::cwd()) eq $base_path ) {
+		delete $args{prefix};
+	}
+	return $args{_self} if $args{_self};
+
+	$args{dispatch} ||= 'Admin';
+	$args{prefix}   ||= 'inc';
+	$args{author}   ||= ($^O eq 'VMS' ? '_author' : '.author');
+	$args{bundle}   ||= 'inc/BUNDLES';
+	$args{base}     ||= $base_path;
+	$class =~ s/^\Q$args{prefix}\E:://;
+	$args{name}     ||= $class;
+	$args{version}  ||= $class->VERSION;
+	unless ( $args{path} ) {
+		$args{path}  = $args{name};
+		$args{path}  =~ s!::!/!g;
+	}
+	$args{file}     ||= "$args{base}/$args{prefix}/$args{path}.pm";
+	$args{wrote}      = 0;
+
+	bless( \%args, $class );
+}
+
+sub call {
+	my ($self, $method) = @_;
+	my $obj = $self->load($method) or return;
+        splice(@_, 0, 2, $obj);
+	goto &{$obj->can($method)};
+}
+
+sub load {
+	my ($self, $method) = @_;
+
+	$self->load_extensions(
+		"$self->{prefix}/$self->{path}", $self
+	) unless $self->{extensions};
+
+	foreach my $obj (@{$self->{extensions}}) {
+		return $obj if $obj->can($method);
+	}
+
+	my $admin = $self->{admin} or die <<"END_DIE";
+The '$method' method does not exist in the '$self->{prefix}' path!
+Please remove the '$self->{prefix}' directory and run $0 again to load it.
+END_DIE
+
+	my $obj = $admin->load($method, 1);
+	push @{$self->{extensions}}, $obj;
+
+	$obj;
+}
+
+sub load_extensions {
+	my ($self, $path, $top) = @_;
+
+	my $should_reload = 0;
+	unless ( grep { ! ref $_ and lc $_ eq lc $self->{prefix} } @INC ) {
+		unshift @INC, $self->{prefix};
+		$should_reload = 1;
+	}
+
+	foreach my $rv ( $self->find_extensions($path) ) {
+		my ($file, $pkg) = @{$rv};
+		next if $self->{pathnames}{$pkg};
+
+		local $@;
+		my $new = eval { local $^W; require $file; $pkg->can('new') };
+		unless ( $new ) {
+			warn $@ if $@;
+			next;
+		}
+		$self->{pathnames}{$pkg} =
+			$should_reload ? delete $INC{$file} : $INC{$file};
+		push @{$self->{extensions}}, &{$new}($pkg, _top => $top );
+	}
+
+	$self->{extensions} ||= [];
+}
+
+sub find_extensions {
+	my ($self, $path) = @_;
+
+	my @found;
+	File::Find::find( sub {
+		my $file = $File::Find::name;
+		return unless $file =~ m!^\Q$path\E/(.+)\.pm\Z!is;
+		my $subpath = $1;
+		return if lc($subpath) eq lc($self->{dispatch});
+
+		$file = "$self->{path}/$subpath.pm";
+		my $pkg = "$self->{name}::$subpath";
+		$pkg =~ s!/!::!g;
+
+		# If we have a mixed-case package name, assume case has been preserved
+		# correctly.  Otherwise, root through the file to locate the case-preserved
+		# version of the package name.
+		if ( $subpath eq lc($subpath) || $subpath eq uc($subpath) ) {
+			my $content = Module::Install::_read($subpath . '.pm');
+			my $in_pod  = 0;
+			foreach ( split //, $content ) {
+				$in_pod = 1 if /^=\w/;
+				$in_pod = 0 if /^=cut/;
+				next if ($in_pod || /^=cut/);  # skip pod text
+				next if /^\s*#/;               # and comments
+				if ( m/^\s*package\s+($pkg)\s*;/i ) {
+					$pkg = $1;
+					last;
+				}
+			}
+		}
+
+		push @found, [ $file, $pkg ];
+	}, $path ) if -d $path;
+
+	@found;
+}
+
+
+
+
+
+#####################################################################
+# Common Utility Functions
+
+sub _caller {
+	my $depth = 0;
+	my $call  = caller($depth);
+	while ( $call eq __PACKAGE__ ) {
+		$depth++;
+		$call = caller($depth);
+	}
+	return $call;
+}
+
+# Done in evals to avoid confusing Perl::MinimumVersion
+eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
+sub _read {
+	local *FH;
+	open( FH, '<', $_[0] ) or die "open($_[0]): $!";
+	my $string = do { local $/; <FH> };
+	close FH or die "close($_[0]): $!";
+	return $string;
+}
+END_NEW
+sub _read {
+	local *FH;
+	open( FH, "< $_[0]"  ) or die "open($_[0]): $!";
+	my $string = do { local $/; <FH> };
+	close FH or die "close($_[0]): $!";
+	return $string;
+}
+END_OLD
+
+sub _readperl {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	$string =~ s/(\n)\n*__(?:DATA|END)__\b.*\z/$1/s;
+	$string =~ s/\n\n=\w+.+?\n\n=cut\b.+?\n+/\n\n/sg;
+	return $string;
+}
+
+sub _readpod {
+	my $string = Module::Install::_read($_[0]);
+	$string =~ s/(?:\015{1,2}\012|\015|\012)/\n/sg;
+	return $string if $_[0] =~ /\.pod\z/;
+	$string =~ s/(^|\n=cut\b.+?\n+)[^=\s].+?\n(\n=\w+|\z)/$1$2/sg;
+	$string =~ s/\n*=pod\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/\n*=cut\b[^\n]*\n+/\n\n/sg;
+	$string =~ s/^\n+//s;
+	return $string;
+}
+
+# Done in evals to avoid confusing Perl::MinimumVersion
+eval( $] >= 5.006 ? <<'END_NEW' : <<'END_OLD' ); die $@ if $@;
+sub _write {
+	local *FH;
+	open( FH, '>', $_[0] ) or die "open($_[0]): $!";
+	foreach ( 1 .. $#_ ) {
+		print FH $_[$_] or die "print($_[0]): $!";
+	}
+	close FH or die "close($_[0]): $!";
+}
+END_NEW
+sub _write {
+	local *FH;
+	open( FH, "> $_[0]"  ) or die "open($_[0]): $!";
+	foreach ( 1 .. $#_ ) {
+		print FH $_[$_] or die "print($_[0]): $!";
+	}
+	close FH or die "close($_[0]): $!";
+}
+END_OLD
+
+# _version is for processing module versions (eg, 1.03_05) not
+# Perl versions (eg, 5.8.1).
+sub _version ($) {
+	my $s = shift || 0;
+	my $d =()= $s =~ /(\.)/g;
+	if ( $d >= 2 ) {
+		# Normalise multipart versions
+		$s =~ s/(\.)(\d{1,3})/sprintf("$1%03d",$2)/eg;
+	}
+	$s =~ s/^(\d+)\.?//;
+	my $l = $1 || 0;
+	my @v = map {
+		$_ . '0' x (3 - length $_)
+	} $s =~ /(\d{1,3})\D?/g;
+	$l = $l . '.' . join '', @v if @v;
+	return $l + 0;
+}
+
+sub _cmp ($$) {
+	_version($_[1]) <=> _version($_[2]);
+}
+
+# Cloned from Params::Util::_CLASS
+sub _CLASS ($) {
+	(
+		defined $_[0]
+		and
+		! ref $_[0]
+		and
+		$_[0] =~ m/^[^\W\d]\w*(?:::\w+)*\z/s
+	) ? $_[0] : undef;
+}
+
+1;
+
+# Copyright 2008 - 2012 Adam Kennedy.
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
new file mode 100644
index 0000000..802844a
--- /dev/null
+++ b/inc/Module/Install/Base.pm
@@ -0,0 +1,83 @@
+#line 1
+package Module::Install::Base;
+
+use strict 'vars';
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = '1.06';
+}
+
+# Suspend handler for "redefined" warnings
+BEGIN {
+	my $w = $SIG{__WARN__};
+	$SIG{__WARN__} = sub { $w };
+}
+
+#line 42
+
+sub new {
+	my $class = shift;
+	unless ( defined &{"${class}::call"} ) {
+		*{"${class}::call"} = sub { shift->_top->call(@_) };
+	}
+	unless ( defined &{"${class}::load"} ) {
+		*{"${class}::load"} = sub { shift->_top->load(@_) };
+	}
+	bless { @_ }, $class;
+}
+
+#line 61
+
+sub AUTOLOAD {
+	local $@;
+	my $func = eval { shift->_top->autoload } or return;
+	goto &$func;
+}
+
+#line 75
+
+sub _top {
+	$_[0]->{_top};
+}
+
+#line 90
+
+sub admin {
+	$_[0]->_top->{admin}
+	or
+	Module::Install::Base::FakeAdmin->new;
+}
+
+#line 106
+
+sub is_admin {
+	! $_[0]->admin->isa('Module::Install::Base::FakeAdmin');
+}
+
+sub DESTROY {}
+
+package Module::Install::Base::FakeAdmin;
+
+use vars qw{$VERSION};
+BEGIN {
+	$VERSION = $Module::Install::Base::VERSION;
+}
+
+my $fake;
+
+sub new {
+	$fake ||= bless(\@_, $_[0]);
+}
+
+sub AUTOLOAD {}
+
+sub DESTROY {}
+
+# Restore warning handler
+BEGIN {
+	$SIG{__WARN__} = $SIG{__WARN__}->();
+}
+
+1;
+
+#line 159
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
new file mode 100644
index 0000000..22167b8
--- /dev/null
+++ b/inc/Module/Install/Can.pm
@@ -0,0 +1,154 @@
+#line 1
+package Module::Install::Can;
+
+use strict;
+use Config                ();
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# check if we can load some module
+### Upgrade this to not have to load the module if possible
+sub can_use {
+	my ($self, $mod, $ver) = @_;
+	$mod =~ s{::|\\}{/}g;
+	$mod .= '.pm' unless $mod =~ /\.pm$/i;
+
+	my $pkg = $mod;
+	$pkg =~ s{/}{::}g;
+	$pkg =~ s{\.pm$}{}i;
+
+	local $@;
+	eval { require $mod; $pkg->VERSION($ver || 0); 1 };
+}
+
+# Check if we can run some command
+sub can_run {
+	my ($self, $cmd) = @_;
+
+	my $_cmd = $cmd;
+	return $_cmd if (-x $_cmd or $_cmd = MM->maybe_command($_cmd));
+
+	for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), '.') {
+		next if $dir eq '';
+		require File::Spec;
+		my $abs = File::Spec->catfile($dir, $cmd);
+		return $abs if (-x $abs or $abs = MM->maybe_command($abs));
+	}
+
+	return;
+}
+
+# Can our C compiler environment build XS files
+sub can_xs {
+	my $self = shift;
+
+	# Ensure we have the CBuilder module
+	$self->configure_requires( 'ExtUtils::CBuilder' => 0.27 );
+
+	# Do we have the configure_requires checker?
+	local $@;
+	eval "require ExtUtils::CBuilder;";
+	if ( $@ ) {
+		# They don't obey configure_requires, so it is
+		# someone old and delicate. Try to avoid hurting
+		# them by falling back to an older simpler test.
+		return $self->can_cc();
+	}
+
+	# Do we have a working C compiler
+	my $builder = ExtUtils::CBuilder->new(
+		quiet => 1,
+	);
+	unless ( $builder->have_compiler ) {
+		# No working C compiler
+		return 0;
+	}
+
+	# Write a C file representative of what XS becomes
+	require File::Temp;
+	my ( $FH, $tmpfile ) = File::Temp::tempfile(
+		"compilexs-XXXXX",
+		SUFFIX => '.c',
+	);
+	binmode $FH;
+	print $FH <<'END_C';
+#include "EXTERN.h"
+#include "perl.h"
+#include "XSUB.h"
+
+int main(int argc, char **argv) {
+    return 0;
+}
+
+int boot_sanexs() {
+    return 1;
+}
+
+END_C
+	close $FH;
+
+	# Can the C compiler access the same headers XS does
+	my @libs   = ();
+	my $object = undef;
+	eval {
+		local $^W = 0;
+		$object = $builder->compile(
+			source => $tmpfile,
+		);
+		@libs = $builder->link(
+			objects     => $object,
+			module_name => 'sanexs',
+		);
+	};
+	my $result = $@ ? 0 : 1;
+
+	# Clean up all the build files
+	foreach ( $tmpfile, $object, @libs ) {
+		next unless defined $_;
+		1 while unlink;
+	}
+
+	return $result;
+}
+
+# Can we locate a (the) C compiler
+sub can_cc {
+	my $self   = shift;
+	my @chunks = split(/ /, $Config::Config{cc}) or return;
+
+	# $Config{cc} may contain args; try to find out the program part
+	while (@chunks) {
+		return $self->can_run("@chunks") || (pop(@chunks), next);
+	}
+
+	return;
+}
+
+# Fix Cygwin bug on maybe_command();
+if ( $^O eq 'cygwin' ) {
+	require ExtUtils::MM_Cygwin;
+	require ExtUtils::MM_Win32;
+	if ( ! defined(&ExtUtils::MM_Cygwin::maybe_command) ) {
+		*ExtUtils::MM_Cygwin::maybe_command = sub {
+			my ($self, $file) = @_;
+			if ($file =~ m{^/cygdrive/}i and ExtUtils::MM_Win32->can('maybe_command')) {
+				ExtUtils::MM_Win32->maybe_command($file);
+			} else {
+				ExtUtils::MM_Unix->maybe_command($file);
+			}
+		}
+	}
+}
+
+1;
+
+__END__
+
+#line 236
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
new file mode 100644
index 0000000..bee0c4f
--- /dev/null
+++ b/inc/Module/Install/Fetch.pm
@@ -0,0 +1,93 @@
+#line 1
+package Module::Install::Fetch;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub get_file {
+    my ($self, %args) = @_;
+    my ($scheme, $host, $path, $file) =
+        $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+
+    if ( $scheme eq 'http' and ! eval { require LWP::Simple; 1 } ) {
+        $args{url} = $args{ftp_url}
+            or (warn("LWP support unavailable!\n"), return);
+        ($scheme, $host, $path, $file) =
+            $args{url} =~ m|^(\w+)://([^/]+)(.+)/(.+)| or return;
+    }
+
+    $|++;
+    print "Fetching '$file' from $host... ";
+
+    unless (eval { require Socket; Socket::inet_aton($host) }) {
+        warn "'$host' resolve failed!\n";
+        return;
+    }
+
+    return unless $scheme eq 'ftp' or $scheme eq 'http';
+
+    require Cwd;
+    my $dir = Cwd::getcwd();
+    chdir $args{local_dir} or return if exists $args{local_dir};
+
+    if (eval { require LWP::Simple; 1 }) {
+        LWP::Simple::mirror($args{url}, $file);
+    }
+    elsif (eval { require Net::FTP; 1 }) { eval {
+        # use Net::FTP to get past firewall
+        my $ftp = Net::FTP->new($host, Passive => 1, Timeout => 600);
+        $ftp->login("anonymous", 'anonymous at example.com');
+        $ftp->cwd($path);
+        $ftp->binary;
+        $ftp->get($file) or (warn("$!\n"), return);
+        $ftp->quit;
+    } }
+    elsif (my $ftp = $self->can_run('ftp')) { eval {
+        # no Net::FTP, fallback to ftp.exe
+        require FileHandle;
+        my $fh = FileHandle->new;
+
+        local $SIG{CHLD} = 'IGNORE';
+        unless ($fh->open("|$ftp -n")) {
+            warn "Couldn't open ftp: $!\n";
+            chdir $dir; return;
+        }
+
+        my @dialog = split(/\n/, <<"END_FTP");
+open $host
+user anonymous anonymous\@example.com
+cd $path
+binary
+get $file $file
+quit
+END_FTP
+        foreach (@dialog) { $fh->print("$_\n") }
+        $fh->close;
+    } }
+    else {
+        warn "No working 'ftp' program available!\n";
+        chdir $dir; return;
+    }
+
+    unless (-f $file) {
+        warn "Fetching failed: $@\n";
+        chdir $dir; return;
+    }
+
+    return if exists $args{size} and -s $file != $args{size};
+    system($args{run}) if exists $args{run};
+    unlink($file) if $args{remove};
+
+    print(((!exists $args{check_for} or -e $args{check_for})
+        ? "done!" : "failed! ($!)"), "\n");
+    chdir $dir; return !$?;
+}
+
+1;
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
new file mode 100644
index 0000000..8310e4c
--- /dev/null
+++ b/inc/Module/Install/Include.pm
@@ -0,0 +1,34 @@
+#line 1
+package Module::Install::Include;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub include {
+	shift()->admin->include(@_);
+}
+
+sub include_deps {
+	shift()->admin->include_deps(@_);
+}
+
+sub auto_include {
+	shift()->admin->auto_include(@_);
+}
+
+sub auto_include_deps {
+	shift()->admin->auto_include_deps(@_);
+}
+
+sub auto_include_dependent_dists {
+	shift()->admin->auto_include_dependent_dists(@_);
+}
+
+1;
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
new file mode 100644
index 0000000..7052f36
--- /dev/null
+++ b/inc/Module/Install/Makefile.pm
@@ -0,0 +1,418 @@
+#line 1
+package Module::Install::Makefile;
+
+use strict 'vars';
+use ExtUtils::MakeMaker   ();
+use Module::Install::Base ();
+use Fcntl qw/:flock :seek/;
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+sub Makefile { $_[0] }
+
+my %seen = ();
+
+sub prompt {
+	shift;
+
+	# Infinite loop protection
+	my @c = caller();
+	if ( ++$seen{"$c[1]|$c[2]|$_[0]"} > 3 ) {
+		die "Caught an potential prompt infinite loop ($c[1]|$c[2]|$_[0])";
+	}
+
+	# In automated testing or non-interactive session, always use defaults
+	if ( ($ENV{AUTOMATED_TESTING} or -! -t STDIN) and ! $ENV{PERL_MM_USE_DEFAULT} ) {
+		local $ENV{PERL_MM_USE_DEFAULT} = 1;
+		goto &ExtUtils::MakeMaker::prompt;
+	} else {
+		goto &ExtUtils::MakeMaker::prompt;
+	}
+}
+
+# Store a cleaned up version of the MakeMaker version,
+# since we need to behave differently in a variety of
+# ways based on the MM version.
+my $makemaker = eval $ExtUtils::MakeMaker::VERSION;
+
+# If we are passed a param, do a "newer than" comparison.
+# Otherwise, just return the MakeMaker version.
+sub makemaker {
+	( @_ < 2 or $makemaker >= eval($_[1]) ) ? $makemaker : 0
+}
+
+# Ripped from ExtUtils::MakeMaker 6.56, and slightly modified
+# as we only need to know here whether the attribute is an array
+# or a hash or something else (which may or may not be appendable).
+my %makemaker_argtype = (
+ C                  => 'ARRAY',
+ CONFIG             => 'ARRAY',
+# CONFIGURE          => 'CODE', # ignore
+ DIR                => 'ARRAY',
+ DL_FUNCS           => 'HASH',
+ DL_VARS            => 'ARRAY',
+ EXCLUDE_EXT        => 'ARRAY',
+ EXE_FILES          => 'ARRAY',
+ FUNCLIST           => 'ARRAY',
+ H                  => 'ARRAY',
+ IMPORTS            => 'HASH',
+ INCLUDE_EXT        => 'ARRAY',
+ LIBS               => 'ARRAY', # ignore ''
+ MAN1PODS           => 'HASH',
+ MAN3PODS           => 'HASH',
+ META_ADD           => 'HASH',
+ META_MERGE         => 'HASH',
+ PL_FILES           => 'HASH',
+ PM                 => 'HASH',
+ PMLIBDIRS          => 'ARRAY',
+ PMLIBPARENTDIRS    => 'ARRAY',
+ PREREQ_PM          => 'HASH',
+ CONFIGURE_REQUIRES => 'HASH',
+ SKIP               => 'ARRAY',
+ TYPEMAPS           => 'ARRAY',
+ XS                 => 'HASH',
+# VERSION            => ['version',''],  # ignore
+# _KEEP_AFTER_FLUSH  => '',
+
+ clean      => 'HASH',
+ depend     => 'HASH',
+ dist       => 'HASH',
+ dynamic_lib=> 'HASH',
+ linkext    => 'HASH',
+ macro      => 'HASH',
+ postamble  => 'HASH',
+ realclean  => 'HASH',
+ test       => 'HASH',
+ tool_autosplit => 'HASH',
+
+ # special cases where you can use makemaker_append
+ CCFLAGS   => 'APPENDABLE',
+ DEFINE    => 'APPENDABLE',
+ INC       => 'APPENDABLE',
+ LDDLFLAGS => 'APPENDABLE',
+ LDFROM    => 'APPENDABLE',
+);
+
+sub makemaker_args {
+	my ($self, %new_args) = @_;
+	my $args = ( $self->{makemaker_args} ||= {} );
+	foreach my $key (keys %new_args) {
+		if ($makemaker_argtype{$key}) {
+			if ($makemaker_argtype{$key} eq 'ARRAY') {
+				$args->{$key} = [] unless defined $args->{$key};
+				unless (ref $args->{$key} eq 'ARRAY') {
+					$args->{$key} = [$args->{$key}]
+				}
+				push @{$args->{$key}},
+					ref $new_args{$key} eq 'ARRAY'
+						? @{$new_args{$key}}
+						: $new_args{$key};
+			}
+			elsif ($makemaker_argtype{$key} eq 'HASH') {
+				$args->{$key} = {} unless defined $args->{$key};
+				foreach my $skey (keys %{ $new_args{$key} }) {
+					$args->{$key}{$skey} = $new_args{$key}{$skey};
+				}
+			}
+			elsif ($makemaker_argtype{$key} eq 'APPENDABLE') {
+				$self->makemaker_append($key => $new_args{$key});
+			}
+		}
+		else {
+			if (defined $args->{$key}) {
+				warn qq{MakeMaker attribute "$key" is overriden; use "makemaker_append" to append values\n};
+			}
+			$args->{$key} = $new_args{$key};
+		}
+	}
+	return $args;
+}
+
+# For mm args that take multiple space-seperated args,
+# append an argument to the current list.
+sub makemaker_append {
+	my $self = shift;
+	my $name = shift;
+	my $args = $self->makemaker_args;
+	$args->{$name} = defined $args->{$name}
+		? join( ' ', $args->{$name}, @_ )
+		: join( ' ', @_ );
+}
+
+sub build_subdirs {
+	my $self    = shift;
+	my $subdirs = $self->makemaker_args->{DIR} ||= [];
+	for my $subdir (@_) {
+		push @$subdirs, $subdir;
+	}
+}
+
+sub clean_files {
+	my $self  = shift;
+	my $clean = $self->makemaker_args->{clean} ||= {};
+	  %$clean = (
+		%$clean,
+		FILES => join ' ', grep { length $_ } ($clean->{FILES} || (), @_),
+	);
+}
+
+sub realclean_files {
+	my $self      = shift;
+	my $realclean = $self->makemaker_args->{realclean} ||= {};
+	  %$realclean = (
+		%$realclean,
+		FILES => join ' ', grep { length $_ } ($realclean->{FILES} || (), @_),
+	);
+}
+
+sub libs {
+	my $self = shift;
+	my $libs = ref $_[0] ? shift : [ shift ];
+	$self->makemaker_args( LIBS => $libs );
+}
+
+sub inc {
+	my $self = shift;
+	$self->makemaker_args( INC => shift );
+}
+
+sub _wanted_t {
+}
+
+sub tests_recursive {
+	my $self = shift;
+	my $dir = shift || 't';
+	unless ( -d $dir ) {
+		die "tests_recursive dir '$dir' does not exist";
+	}
+	my %tests = map { $_ => 1 } split / /, ($self->tests || '');
+	require File::Find;
+	File::Find::find(
+        sub { /\.t$/ and -f $_ and $tests{"$File::Find::dir/*.t"} = 1 },
+        $dir
+    );
+	$self->tests( join ' ', sort keys %tests );
+}
+
+sub write {
+	my $self = shift;
+	die "&Makefile->write() takes no arguments\n" if @_;
+
+	# Check the current Perl version
+	my $perl_version = $self->perl_version;
+	if ( $perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+	}
+
+	# Make sure we have a new enough MakeMaker
+	require ExtUtils::MakeMaker;
+
+	if ( $perl_version and $self->_cmp($perl_version, '5.006') >= 0 ) {
+		# This previous attempted to inherit the version of
+		# ExtUtils::MakeMaker in use by the module author, but this
+		# was found to be untenable as some authors build releases
+		# using future dev versions of EU:MM that nobody else has.
+		# Instead, #toolchain suggests we use 6.59 which is the most
+		# stable version on CPAN at time of writing and is, to quote
+		# ribasushi, "not terminally fucked, > and tested enough".
+		# TODO: We will now need to maintain this over time to push
+		# the version up as new versions are released.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.59 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.59 );
+	} else {
+		# Allow legacy-compatibility with 5.005 by depending on the
+		# most recent EU:MM that supported 5.005.
+		$self->build_requires(     'ExtUtils::MakeMaker' => 6.36 );
+		$self->configure_requires( 'ExtUtils::MakeMaker' => 6.36 );
+	}
+
+	# Generate the MakeMaker params
+	my $args = $self->makemaker_args;
+	$args->{DISTNAME} = $self->name;
+	$args->{NAME}     = $self->module_name || $self->name;
+	$args->{NAME}     =~ s/-/::/g;
+	$args->{VERSION}  = $self->version or die <<'EOT';
+ERROR: Can't determine distribution version. Please specify it
+explicitly via 'version' in Makefile.PL, or set a valid $VERSION
+in a module, and provide its file path via 'version_from' (or
+'all_from' if you prefer) in Makefile.PL.
+EOT
+
+	if ( $self->tests ) {
+		my @tests = split ' ', $self->tests;
+		my %seen;
+		$args->{test} = {
+			TESTS => (join ' ', grep {!$seen{$_}++} @tests),
+		};
+    } elsif ( $Module::Install::ExtraTests::use_extratests ) {
+        # Module::Install::ExtraTests doesn't set $self->tests and does its own tests via harness.
+        # So, just ignore our xt tests here.
+	} elsif ( -d 'xt' and ($Module::Install::AUTHOR or $ENV{RELEASE_TESTING}) ) {
+		$args->{test} = {
+			TESTS => join( ' ', map { "$_/*.t" } grep { -d $_ } qw{ t xt } ),
+		};
+	}
+	if ( $] >= 5.005 ) {
+		$args->{ABSTRACT} = $self->abstract;
+		$args->{AUTHOR}   = join ', ', @{$self->author || []};
+	}
+	if ( $self->makemaker(6.10) ) {
+		$args->{NO_META}   = 1;
+		#$args->{NO_MYMETA} = 1;
+	}
+	if ( $self->makemaker(6.17) and $self->sign ) {
+		$args->{SIGN} = 1;
+	}
+	unless ( $self->is_admin ) {
+		delete $args->{SIGN};
+	}
+	if ( $self->makemaker(6.31) and $self->license ) {
+		$args->{LICENSE} = $self->license;
+	}
+
+	my $prereq = ($args->{PREREQ_PM} ||= {});
+	%$prereq = ( %$prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->requires)
+	);
+
+	# Remove any reference to perl, PREREQ_PM doesn't support it
+	delete $args->{PREREQ_PM}->{perl};
+
+	# Merge both kinds of requires into BUILD_REQUIRES
+	my $build_prereq = ($args->{BUILD_REQUIRES} ||= {});
+	%$build_prereq = ( %$build_prereq,
+		map { @$_ } # flatten [module => version]
+		map { @$_ }
+		grep $_,
+		($self->configure_requires, $self->build_requires)
+	);
+
+	# Remove any reference to perl, BUILD_REQUIRES doesn't support it
+	delete $args->{BUILD_REQUIRES}->{perl};
+
+	# Delete bundled dists from prereq_pm, add it to Makefile DIR
+	my $subdirs = ($args->{DIR} || []);
+	if ($self->bundles) {
+		my %processed;
+		foreach my $bundle (@{ $self->bundles }) {
+			my ($mod_name, $dist_dir) = @$bundle;
+			delete $prereq->{$mod_name};
+			$dist_dir = File::Basename::basename($dist_dir); # dir for building this module
+			if (not exists $processed{$dist_dir}) {
+				if (-d $dist_dir) {
+					# List as sub-directory to be processed by make
+					push @$subdirs, $dist_dir;
+				}
+				# Else do nothing: the module is already present on the system
+				$processed{$dist_dir} = undef;
+			}
+		}
+	}
+
+	unless ( $self->makemaker('6.55_03') ) {
+		%$prereq = (%$prereq,%$build_prereq);
+		delete $args->{BUILD_REQUIRES};
+	}
+
+	if ( my $perl_version = $self->perl_version ) {
+		eval "use $perl_version; 1"
+			or die "ERROR: perl: Version $] is installed, "
+			. "but we need version >= $perl_version";
+
+		if ( $self->makemaker(6.48) ) {
+			$args->{MIN_PERL_VERSION} = $perl_version;
+		}
+	}
+
+	if ($self->installdirs) {
+		warn qq{old INSTALLDIRS (probably set by makemaker_args) is overriden by installdirs\n} if $args->{INSTALLDIRS};
+		$args->{INSTALLDIRS} = $self->installdirs;
+	}
+
+	my %args = map {
+		( $_ => $args->{$_} ) } grep {defined($args->{$_} )
+	} keys %$args;
+
+	my $user_preop = delete $args{dist}->{PREOP};
+	if ( my $preop = $self->admin->preop($user_preop) ) {
+		foreach my $key ( keys %$preop ) {
+			$args{dist}->{$key} = $preop->{$key};
+		}
+	}
+
+	my $mm = ExtUtils::MakeMaker::WriteMakefile(%args);
+	$self->fix_up_makefile($mm->{FIRST_MAKEFILE} || 'Makefile');
+}
+
+sub fix_up_makefile {
+	my $self          = shift;
+	my $makefile_name = shift;
+	my $top_class     = ref($self->_top) || '';
+	my $top_version   = $self->_top->VERSION || '';
+
+	my $preamble = $self->preamble
+		? "# Preamble by $top_class $top_version\n"
+			. $self->preamble
+		: '';
+	my $postamble = "# Postamble by $top_class $top_version\n"
+		. ($self->postamble || '');
+
+	local *MAKEFILE;
+	open MAKEFILE, "+< $makefile_name" or die "fix_up_makefile: Couldn't open $makefile_name: $!";
+	eval { flock MAKEFILE, LOCK_EX };
+	my $makefile = do { local $/; <MAKEFILE> };
+
+	$makefile =~ s/\b(test_harness\(\$\(TEST_VERBOSE\), )/$1'inc', /;
+	$makefile =~ s/( -I\$\(INST_ARCHLIB\))/ -Iinc$1/g;
+	$makefile =~ s/( "-I\$\(INST_LIB\)")/ "-Iinc"$1/g;
+	$makefile =~ s/^(FULLPERL = .*)/$1 "-Iinc"/m;
+	$makefile =~ s/^(PERL = .*)/$1 "-Iinc"/m;
+
+	# Module::Install will never be used to build the Core Perl
+	# Sometimes PERL_LIB and PERL_ARCHLIB get written anyway, which breaks
+	# PREFIX/PERL5LIB, and thus, install_share. Blank them if they exist
+	$makefile =~ s/^PERL_LIB = .+/PERL_LIB =/m;
+	#$makefile =~ s/^PERL_ARCHLIB = .+/PERL_ARCHLIB =/m;
+
+	# Perl 5.005 mentions PERL_LIB explicitly, so we have to remove that as well.
+	$makefile =~ s/(\"?)-I\$\(PERL_LIB\)\1//g;
+
+	# XXX - This is currently unused; not sure if it breaks other MM-users
+	# $makefile =~ s/^pm_to_blib\s+:\s+/pm_to_blib :: /mg;
+
+	seek MAKEFILE, 0, SEEK_SET;
+	truncate MAKEFILE, 0;
+	print MAKEFILE  "$preamble$makefile$postamble" or die $!;
+	close MAKEFILE  or die $!;
+
+	1;
+}
+
+sub preamble {
+	my ($self, $text) = @_;
+	$self->{preamble} = $text . $self->{preamble} if defined $text;
+	$self->{preamble};
+}
+
+sub postamble {
+	my ($self, $text) = @_;
+	$self->{postamble} ||= $self->admin->postamble;
+	$self->{postamble} .= $text if defined $text;
+	$self->{postamble}
+}
+
+1;
+
+__END__
+
+#line 544
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
new file mode 100644
index 0000000..58430f3
--- /dev/null
+++ b/inc/Module/Install/Metadata.pm
@@ -0,0 +1,722 @@
+#line 1
+package Module::Install::Metadata;
+
+use strict 'vars';
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+my @boolean_keys = qw{
+	sign
+};
+
+my @scalar_keys = qw{
+	name
+	module_name
+	abstract
+	version
+	distribution_type
+	tests
+	installdirs
+};
+
+my @tuple_keys = qw{
+	configure_requires
+	build_requires
+	requires
+	recommends
+	bundles
+	resources
+};
+
+my @resource_keys = qw{
+	homepage
+	bugtracker
+	repository
+};
+
+my @array_keys = qw{
+	keywords
+	author
+};
+
+*authors = \&author;
+
+sub Meta              { shift          }
+sub Meta_BooleanKeys  { @boolean_keys  }
+sub Meta_ScalarKeys   { @scalar_keys   }
+sub Meta_TupleKeys    { @tuple_keys    }
+sub Meta_ResourceKeys { @resource_keys }
+sub Meta_ArrayKeys    { @array_keys    }
+
+foreach my $key ( @boolean_keys ) {
+	*$key = sub {
+		my $self = shift;
+		if ( defined wantarray and not @_ ) {
+			return $self->{values}->{$key};
+		}
+		$self->{values}->{$key} = ( @_ ? $_[0] : 1 );
+		return $self;
+	};
+}
+
+foreach my $key ( @scalar_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} = shift;
+		return $self;
+	};
+}
+
+foreach my $key ( @array_keys ) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} if defined wantarray and !@_;
+		$self->{values}->{$key} ||= [];
+		push @{$self->{values}->{$key}}, @_;
+		return $self;
+	};
+}
+
+foreach my $key ( @resource_keys ) {
+	*$key = sub {
+		my $self = shift;
+		unless ( @_ ) {
+			return () unless $self->{values}->{resources};
+			return map  { $_->[1] }
+			       grep { $_->[0] eq $key }
+			       @{ $self->{values}->{resources} };
+		}
+		return $self->{values}->{resources}->{$key} unless @_;
+		my $uri = shift or die(
+			"Did not provide a value to $key()"
+		);
+		$self->resources( $key => $uri );
+		return 1;
+	};
+}
+
+foreach my $key ( grep { $_ ne "resources" } @tuple_keys) {
+	*$key = sub {
+		my $self = shift;
+		return $self->{values}->{$key} unless @_;
+		my @added;
+		while ( @_ ) {
+			my $module  = shift or last;
+			my $version = shift || 0;
+			push @added, [ $module, $version ];
+		}
+		push @{ $self->{values}->{$key} }, @added;
+		return map {@$_} @added;
+	};
+}
+
+# Resource handling
+my %lc_resource = map { $_ => 1 } qw{
+	homepage
+	license
+	bugtracker
+	repository
+};
+
+sub resources {
+	my $self = shift;
+	while ( @_ ) {
+		my $name  = shift or last;
+		my $value = shift or next;
+		if ( $name eq lc $name and ! $lc_resource{$name} ) {
+			die("Unsupported reserved lowercase resource '$name'");
+		}
+		$self->{values}->{resources} ||= [];
+		push @{ $self->{values}->{resources} }, [ $name, $value ];
+	}
+	$self->{values}->{resources};
+}
+
+# Aliases for build_requires that will have alternative
+# meanings in some future version of META.yml.
+sub test_requires     { shift->build_requires(@_) }
+sub install_requires  { shift->build_requires(@_) }
+
+# Aliases for installdirs options
+sub install_as_core   { $_[0]->installdirs('perl')   }
+sub install_as_cpan   { $_[0]->installdirs('site')   }
+sub install_as_site   { $_[0]->installdirs('site')   }
+sub install_as_vendor { $_[0]->installdirs('vendor') }
+
+sub dynamic_config {
+	my $self  = shift;
+	my $value = @_ ? shift : 1;
+	if ( $self->{values}->{dynamic_config} ) {
+		# Once dynamic we never change to static, for safety
+		return 0;
+	}
+	$self->{values}->{dynamic_config} = $value ? 1 : 0;
+	return 1;
+}
+
+# Convenience command
+sub static_config {
+	shift->dynamic_config(0);
+}
+
+sub perl_version {
+	my $self = shift;
+	return $self->{values}->{perl_version} unless @_;
+	my $version = shift or die(
+		"Did not provide a value to perl_version()"
+	);
+
+	# Normalize the version
+	$version = $self->_perl_version($version);
+
+	# We don't support the really old versions
+	unless ( $version >= 5.005 ) {
+		die "Module::Install only supports 5.005 or newer (use ExtUtils::MakeMaker)\n";
+	}
+
+	$self->{values}->{perl_version} = $version;
+}
+
+sub all_from {
+	my ( $self, $file ) = @_;
+
+	unless ( defined($file) ) {
+		my $name = $self->name or die(
+			"all_from called with no args without setting name() first"
+		);
+		$file = join('/', 'lib', split(/-/, $name)) . '.pm';
+		$file =~ s{.*/}{} unless -e $file;
+		unless ( -e $file ) {
+			die("all_from cannot find $file from $name");
+		}
+	}
+	unless ( -f $file ) {
+		die("The path '$file' does not exist, or is not a file");
+	}
+
+	$self->{values}{all_from} = $file;
+
+	# Some methods pull from POD instead of code.
+	# If there is a matching .pod, use that instead
+	my $pod = $file;
+	$pod =~ s/\.pm$/.pod/i;
+	$pod = $file unless -e $pod;
+
+	# Pull the different values
+	$self->name_from($file)         unless $self->name;
+	$self->version_from($file)      unless $self->version;
+	$self->perl_version_from($file) unless $self->perl_version;
+	$self->author_from($pod)        unless @{$self->author || []};
+	$self->license_from($pod)       unless $self->license;
+	$self->abstract_from($pod)      unless $self->abstract;
+
+	return 1;
+}
+
+sub provides {
+	my $self     = shift;
+	my $provides = ( $self->{values}->{provides} ||= {} );
+	%$provides = (%$provides, @_) if @_;
+	return $provides;
+}
+
+sub auto_provides {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	unless (-e 'MANIFEST') {
+		warn "Cannot deduce auto_provides without a MANIFEST, skipping\n";
+		return $self;
+	}
+	# Avoid spurious warnings as we are not checking manifest here.
+	local $SIG{__WARN__} = sub {1};
+	require ExtUtils::Manifest;
+	local *ExtUtils::Manifest::manicheck = sub { return };
+
+	require Module::Build;
+	my $build = Module::Build->new(
+		dist_name    => $self->name,
+		dist_version => $self->version,
+		license      => $self->license,
+	);
+	$self->provides( %{ $build->find_dist_packages || {} } );
+}
+
+sub feature {
+	my $self     = shift;
+	my $name     = shift;
+	my $features = ( $self->{values}->{features} ||= [] );
+	my $mods;
+
+	if ( @_ == 1 and ref( $_[0] ) ) {
+		# The user used ->feature like ->features by passing in the second
+		# argument as a reference.  Accomodate for that.
+		$mods = $_[0];
+	} else {
+		$mods = \@_;
+	}
+
+	my $count = 0;
+	push @$features, (
+		$name => [
+			map {
+				ref($_) ? ( ref($_) eq 'HASH' ) ? %$_ : @$_ : $_
+			} @$mods
+		]
+	);
+
+	return @$features;
+}
+
+sub features {
+	my $self = shift;
+	while ( my ( $name, $mods ) = splice( @_, 0, 2 ) ) {
+		$self->feature( $name, @$mods );
+	}
+	return $self->{values}->{features}
+		? @{ $self->{values}->{features} }
+		: ();
+}
+
+sub no_index {
+	my $self = shift;
+	my $type = shift;
+	push @{ $self->{values}->{no_index}->{$type} }, @_ if $type;
+	return $self->{values}->{no_index};
+}
+
+sub read {
+	my $self = shift;
+	$self->include_deps( 'YAML::Tiny', 0 );
+
+	require YAML::Tiny;
+	my $data = YAML::Tiny::LoadFile('META.yml');
+
+	# Call methods explicitly in case user has already set some values.
+	while ( my ( $key, $value ) = each %$data ) {
+		next unless $self->can($key);
+		if ( ref $value eq 'HASH' ) {
+			while ( my ( $module, $version ) = each %$value ) {
+				$self->can($key)->($self, $module => $version );
+			}
+		} else {
+			$self->can($key)->($self, $value);
+		}
+	}
+	return $self;
+}
+
+sub write {
+	my $self = shift;
+	return $self unless $self->is_admin;
+	$self->admin->write_meta;
+	return $self;
+}
+
+sub version_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->version( ExtUtils::MM_Unix->parse_version($file) );
+
+	# for version integrity check
+	$self->makemaker_args( VERSION_FROM => $file );
+}
+
+sub abstract_from {
+	require ExtUtils::MM_Unix;
+	my ( $self, $file ) = @_;
+	$self->abstract(
+		bless(
+			{ DISTNAME => $self->name },
+			'ExtUtils::MM_Unix'
+		)->parse_abstract($file)
+	);
+}
+
+# Add both distribution and module name
+sub name_from {
+	my ($self, $file) = @_;
+	if (
+		Module::Install::_read($file) =~ m/
+		^ \s*
+		package \s*
+		([\w:]+)
+		\s* ;
+		/ixms
+	) {
+		my ($name, $module_name) = ($1, $1);
+		$name =~ s{::}{-}g;
+		$self->name($name);
+		unless ( $self->module_name ) {
+			$self->module_name($module_name);
+		}
+	} else {
+		die("Cannot determine name from $file\n");
+	}
+}
+
+sub _extract_perl_version {
+	if (
+		$_[0] =~ m/
+		^\s*
+		(?:use|require) \s*
+		v?
+		([\d_\.]+)
+		\s* ;
+		/ixms
+	) {
+		my $perl_version = $1;
+		$perl_version =~ s{_}{}g;
+		return $perl_version;
+	} else {
+		return;
+	}
+}
+
+sub perl_version_from {
+	my $self = shift;
+	my $perl_version=_extract_perl_version(Module::Install::_read($_[0]));
+	if ($perl_version) {
+		$self->perl_version($perl_version);
+	} else {
+		warn "Cannot determine perl version info from $_[0]\n";
+		return;
+	}
+}
+
+sub author_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	if ($content =~ m/
+		=head \d \s+ (?:authors?)\b \s*
+		([^\n]*)
+		|
+		=head \d \s+ (?:licen[cs]e|licensing|copyright|legal)\b \s*
+		.*? copyright .*? \d\d\d[\d.]+ \s* (?:\bby\b)? \s*
+		([^\n]*)
+	/ixms) {
+		my $author = $1 || $2;
+
+		# XXX: ugly but should work anyway...
+		if (eval "require Pod::Escapes; 1") {
+			# Pod::Escapes has a mapping table.
+			# It's in core of perl >= 5.9.3, and should be installed
+			# as one of the Pod::Simple's prereqs, which is a prereq
+			# of Pod::Text 3.x (see also below).
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $Pod::Escapes::Name2character_number{$1}
+				? chr($Pod::Escapes::Name2character_number{$1})
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		elsif (eval "require Pod::Text; 1" && $Pod::Text::VERSION < 3) {
+			# Pod::Text < 3.0 has yet another mapping table,
+			# though the table name of 2.x and 1.x are different.
+			# (1.x is in core of Perl < 5.6, 2.x is in core of
+			# Perl < 5.9.3)
+			my $mapping = ($Pod::Text::VERSION < 2)
+				? \%Pod::Text::HTML_Escapes
+				: \%Pod::Text::ESCAPES;
+			$author =~ s{ E<( (\d+) | ([A-Za-z]+) )> }
+			{
+				defined $2
+				? chr($2)
+				: defined $mapping->{$1}
+				? $mapping->{$1}
+				: do {
+					warn "Unknown escape: E<$1>";
+					"E<$1>";
+				};
+			}gex;
+		}
+		else {
+			$author =~ s{E<lt>}{<}g;
+			$author =~ s{E<gt>}{>}g;
+		}
+		$self->author($author);
+	} else {
+		warn "Cannot determine author info from $_[0]\n";
+	}
+}
+
+#Stolen from M::B
+my %license_urls = (
+    perl         => 'http://dev.perl.org/licenses/',
+    apache       => 'http://apache.org/licenses/LICENSE-2.0',
+    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
+    artistic     => 'http://opensource.org/licenses/artistic-license.php',
+    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+    lgpl         => 'http://opensource.org/licenses/lgpl-license.php',
+    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
+    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
+    bsd          => 'http://opensource.org/licenses/bsd-license.php',
+    gpl          => 'http://opensource.org/licenses/gpl-license.php',
+    gpl2         => 'http://opensource.org/licenses/gpl-2.0.php',
+    gpl3         => 'http://opensource.org/licenses/gpl-3.0.html',
+    mit          => 'http://opensource.org/licenses/mit-license.php',
+    mozilla      => 'http://opensource.org/licenses/mozilla1.1.php',
+    open_source  => undef,
+    unrestricted => undef,
+    restrictive  => undef,
+    unknown      => undef,
+);
+
+sub license {
+	my $self = shift;
+	return $self->{values}->{license} unless @_;
+	my $license = shift or die(
+		'Did not provide a value to license()'
+	);
+	$license = __extract_license($license) || lc $license;
+	$self->{values}->{license} = $license;
+
+	# Automatically fill in license URLs
+	if ( $license_urls{$license} ) {
+		$self->resources( license => $license_urls{$license} );
+	}
+
+	return 1;
+}
+
+sub _extract_license {
+	my $pod = shift;
+	my $matched;
+	return __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ L(?i:ICEN[CS]E|ICENSING)\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	) || __extract_license(
+		($matched) = $pod =~ m/
+			(=head \d \s+ (?:C(?i:OPYRIGHTS?)|L(?i:EGAL))\b.*?)
+			(=head \d.*|=cut.*|)\z
+		/xms
+	);
+}
+
+sub __extract_license {
+	my $license_text = shift or return;
+	my @phrases      = (
+		'(?:under )?the same (?:terms|license) as (?:perl|the perl (?:\d )?programming language)' => 'perl', 1,
+		'(?:under )?the terms of (?:perl|the perl programming language) itself' => 'perl', 1,
+		'Artistic and GPL'                   => 'perl',         1,
+		'GNU general public license'         => 'gpl',          1,
+		'GNU public license'                 => 'gpl',          1,
+		'GNU lesser general public license'  => 'lgpl',         1,
+		'GNU lesser public license'          => 'lgpl',         1,
+		'GNU library general public license' => 'lgpl',         1,
+		'GNU library public license'         => 'lgpl',         1,
+		'GNU Free Documentation license'     => 'unrestricted', 1,
+		'GNU Affero General Public License'  => 'open_source',  1,
+		'(?:Free)?BSD license'               => 'bsd',          1,
+		'Artistic license 2\.0'              => 'artistic_2',   1,
+		'Artistic license'                   => 'artistic',     1,
+		'Apache (?:Software )?license'       => 'apache',       1,
+		'GPL'                                => 'gpl',          1,
+		'LGPL'                               => 'lgpl',         1,
+		'BSD'                                => 'bsd',          1,
+		'Artistic'                           => 'artistic',     1,
+		'MIT'                                => 'mit',          1,
+		'Mozilla Public License'             => 'mozilla',      1,
+		'Q Public License'                   => 'open_source',  1,
+		'OpenSSL License'                    => 'unrestricted', 1,
+		'SSLeay License'                     => 'unrestricted', 1,
+		'zlib License'                       => 'open_source',  1,
+		'proprietary'                        => 'proprietary',  0,
+	);
+	while ( my ($pattern, $license, $osi) = splice(@phrases, 0, 3) ) {
+		$pattern =~ s#\s+#\\s+#gs;
+		if ( $license_text =~ /\b$pattern\b/i ) {
+			return $license;
+		}
+	}
+	return '';
+}
+
+sub license_from {
+	my $self = shift;
+	if (my $license=_extract_license(Module::Install::_read($_[0]))) {
+		$self->license($license);
+	} else {
+		warn "Cannot determine license info from $_[0]\n";
+		return 'unknown';
+	}
+}
+
+sub _extract_bugtracker {
+	my @links   = $_[0] =~ m#L<(
+	 https?\Q://rt.cpan.org/\E[^>]+|
+	 https?\Q://github.com/\E[\w_]+/[\w_]+/issues|
+	 https?\Q://code.google.com/p/\E[\w_\-]+/issues/list
+	 )>#gx;
+	my %links;
+	@links{@links}=();
+	@links=keys %links;
+	return @links;
+}
+
+sub bugtracker_from {
+	my $self    = shift;
+	my $content = Module::Install::_read($_[0]);
+	my @links   = _extract_bugtracker($content);
+	unless ( @links ) {
+		warn "Cannot determine bugtracker info from $_[0]\n";
+		return 0;
+	}
+	if ( @links > 1 ) {
+		warn "Found more than one bugtracker link in $_[0]\n";
+		return 0;
+	}
+
+	# Set the bugtracker
+	bugtracker( $links[0] );
+	return 1;
+}
+
+sub requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+(v?[\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->requires( $module => $version );
+	}
+}
+
+sub test_requires_from {
+	my $self     = shift;
+	my $content  = Module::Install::_readperl($_[0]);
+	my @requires = $content =~ m/^use\s+([^\W\d]\w*(?:::\w+)*)\s+([\d\.]+)/mg;
+	while ( @requires ) {
+		my $module  = shift @requires;
+		my $version = shift @requires;
+		$self->test_requires( $module => $version );
+	}
+}
+
+# Convert triple-part versions (eg, 5.6.1 or 5.8.9) to
+# numbers (eg, 5.006001 or 5.008009).
+# Also, convert double-part versions (eg, 5.8)
+sub _perl_version {
+	my $v = $_[-1];
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)$/sprintf("%d.%03d",$1,$2)/e;
+	$v =~ s/^([1-9])\.([1-9]\d?\d?)\.(0|[1-9]\d?\d?)$/sprintf("%d.%03d%03d",$1,$2,$3 || 0)/e;
+	$v =~ s/(\.\d\d\d)000$/$1/;
+	$v =~ s/_.+$//;
+	if ( ref($v) ) {
+		# Numify
+		$v = $v + 0;
+	}
+	return $v;
+}
+
+sub add_metadata {
+    my $self = shift;
+    my %hash = @_;
+    for my $key (keys %hash) {
+        warn "add_metadata: $key is not prefixed with 'x_'.\n" .
+             "Use appopriate function to add non-private metadata.\n" unless $key =~ /^x_/;
+        $self->{values}->{$key} = $hash{$key};
+    }
+}
+
+
+######################################################################
+# MYMETA Support
+
+sub WriteMyMeta {
+	die "WriteMyMeta has been deprecated";
+}
+
+sub write_mymeta_yaml {
+	my $self = shift;
+
+	# We need YAML::Tiny to write the MYMETA.yml file
+	unless ( eval { require YAML::Tiny; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.yml\n";
+	YAML::Tiny::DumpFile('MYMETA.yml', $meta);
+}
+
+sub write_mymeta_json {
+	my $self = shift;
+
+	# We need JSON to write the MYMETA.json file
+	unless ( eval { require JSON; 1; } ) {
+		return 1;
+	}
+
+	# Generate the data
+	my $meta = $self->_write_mymeta_data or return 1;
+
+	# Save as the MYMETA.yml file
+	print "Writing MYMETA.json\n";
+	Module::Install::_write(
+		'MYMETA.json',
+		JSON->new->pretty(1)->canonical->encode($meta),
+	);
+}
+
+sub _write_mymeta_data {
+	my $self = shift;
+
+	# If there's no existing META.yml there is nothing we can do
+	return undef unless -f 'META.yml';
+
+	# We need Parse::CPAN::Meta to load the file
+	unless ( eval { require Parse::CPAN::Meta; 1; } ) {
+		return undef;
+	}
+
+	# Merge the perl version into the dependencies
+	my $val  = $self->Meta->{values};
+	my $perl = delete $val->{perl_version};
+	if ( $perl ) {
+		$val->{requires} ||= [];
+		my $requires = $val->{requires};
+
+		# Canonize to three-dot version after Perl 5.6
+		if ( $perl >= 5.006 ) {
+			$perl =~ s{^(\d+)\.(\d\d\d)(\d*)}{join('.', $1, int($2||0), int($3||0))}e
+		}
+		unshift @$requires, [ perl => $perl ];
+	}
+
+	# Load the advisory META.yml file
+	my @yaml = Parse::CPAN::Meta::LoadFile('META.yml');
+	my $meta = $yaml[0];
+
+	# Overwrite the non-configure dependency hashs
+	delete $meta->{requires};
+	delete $meta->{build_requires};
+	delete $meta->{recommends};
+	if ( exists $val->{requires} ) {
+		$meta->{requires} = { map { @$_ } @{ $val->{requires} } };
+	}
+	if ( exists $val->{build_requires} ) {
+		$meta->{build_requires} = { map { @$_ } @{ $val->{build_requires} } };
+	}
+
+	return $meta;
+}
+
+1;
diff --git a/inc/Module/Install/RTx.pm b/inc/Module/Install/RTx.pm
new file mode 100644
index 0000000..3268e7e
--- /dev/null
+++ b/inc/Module/Install/RTx.pm
@@ -0,0 +1,300 @@
+#line 1
+package Module::Install::RTx;
+
+use 5.008;
+use strict;
+use warnings;
+no warnings 'once';
+
+use Module::Install::Base;
+use base 'Module::Install::Base';
+our $VERSION = '0.39';
+
+use FindBin;
+use File::Glob     ();
+use File::Basename ();
+
+my @DIRS = qw(etc lib html static bin sbin po var);
+my @INDEX_DIRS = qw(lib bin sbin);
+
+sub RTx {
+    my ( $self, $name, $extra_args ) = @_;
+    $extra_args ||= {};
+
+    # Set up names
+    my $fname = $name;
+    $fname =~ s!-!/!g;
+
+    $self->name( $name )
+        unless $self->name;
+    $self->all_from( "lib/$fname.pm" )
+        unless $self->version;
+    $self->abstract("$name Extension")
+        unless $self->abstract;
+    unless ( $extra_args->{no_readme_generation} ) {
+        $self->readme_from( "lib/$fname.pm",
+                            { options => [ quotes => "none" ] } );
+    }
+    $self->add_metadata("x_module_install_rtx_version", $VERSION );
+
+    my $installdirs = $ENV{INSTALLDIRS};
+    for ( @ARGV ) {
+        if ( /INSTALLDIRS=(.*)/ ) {
+            $installdirs = $1;
+        }
+    }
+
+    # Try to find RT.pm
+    my @prefixes = qw( /opt /usr/local /home /usr /sw /usr/share/request-tracker4);
+    $ENV{RTHOME} =~ s{/RT\.pm$}{} if defined $ENV{RTHOME};
+    $ENV{RTHOME} =~ s{/lib/?$}{}  if defined $ENV{RTHOME};
+    my @try = $ENV{RTHOME} ? ($ENV{RTHOME}, "$ENV{RTHOME}/lib") : ();
+    while (1) {
+        my @look = @INC;
+        unshift @look, grep {defined and -d $_} @try;
+        push @look, grep {defined and -d $_}
+            map { ( "$_/rt4/lib", "$_/lib/rt4", "$_/lib" ) } @prefixes;
+        last if eval {local @INC = @look; require RT; $RT::LocalLibPath};
+
+        warn
+            "Cannot find the location of RT.pm that defines \$RT::LocalPath in: @look\n";
+        my $given = $self->prompt("Path to directory containing your RT.pm:") or exit;
+        $given =~ s{/RT\.pm$}{};
+        $given =~ s{/lib/?$}{};
+        @try = ($given, "$given/lib");
+    }
+
+    print "Using RT configuration from $INC{'RT.pm'}:\n";
+
+    my $local_lib_path = $RT::LocalLibPath;
+    unshift @INC, $local_lib_path;
+    my $lib_path = File::Basename::dirname( $INC{'RT.pm'} );
+    unshift @INC, $lib_path;
+
+    # Set a baseline minimum version
+    unless ( $extra_args->{deprecated_rt} ) {
+        $self->requires_rt('4.0.0');
+    }
+
+    # Installation locations
+    my %path;
+    my $plugin_path;
+    if ( $installdirs && $installdirs eq 'vendor' ) {
+        $plugin_path = $RT::PluginPath;
+    } else {
+        $plugin_path = $RT::LocalPluginPath;
+    }
+    $path{$_} = $plugin_path . "/$name/$_"
+        foreach @DIRS;
+
+    # Copy RT 4.2.0 static files into NoAuth; insufficient for
+    # images, but good enough for css and js.
+    $path{static} = "$path{html}/NoAuth/"
+        unless $RT::StaticPath;
+
+    # Delete the ones we don't need
+    delete $path{$_} for grep {not -d "$FindBin::Bin/$_"} keys %path;
+
+    my %index = map { $_ => 1 } @INDEX_DIRS;
+    $self->no_index( directory => $_ ) foreach grep !$index{$_}, @DIRS;
+
+    my $args = join ', ', map "q($_)", map { ($_, "\$(DESTDIR)$path{$_}") }
+        sort keys %path;
+
+    printf "%-10s => %s\n", $_, $path{$_} for sort keys %path;
+
+    if ( my @dirs = map { ( -D => $_ ) } grep $path{$_}, qw(bin html sbin etc) ) {
+        my @po = map { ( -o => $_ ) }
+            grep -f,
+            File::Glob::bsd_glob("po/*.po");
+        $self->postamble(<< ".") if @po;
+lexicons ::
+\t\$(NOECHO) \$(PERL) -MLocale::Maketext::Extract::Run=xgettext -e \"xgettext(qw(@dirs @po))\"
+.
+    }
+
+    my $remove_files;
+    if( $extra_args->{'remove_files'} ){
+        $self->include('Module::Install::RTx::Remove');
+        our @remove_files;
+        eval { require "etc/upgrade/remove_files" }
+          or print "No remove file located, no files to remove\n";
+        $remove_files = join ",", map {"q(\$(DESTDIR)$plugin_path/$name/$_)"} @remove_files;
+    }
+
+    $self->include('Module::Install::RTx::Runtime') if $self->admin;
+    $self->include_deps( 'YAML::Tiny', 0 ) if $self->admin;
+    my $postamble = << ".";
+install ::
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxPlugin()"
+.
+
+    if( $remove_files ){
+        $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MModule::Install::RTx::Remove -e \"RTxRemove([$remove_files])\"
+.
+    }
+
+    $postamble .= << ".";
+\t\$(NOECHO) \$(PERL) -MExtUtils::Install -e \"install({$args})\"
+.
+
+    if ( $path{var} and -d $RT::MasonDataDir ) {
+        my ( $uid, $gid ) = ( stat($RT::MasonDataDir) )[ 4, 5 ];
+        $postamble .= << ".";
+\t\$(NOECHO) chown -R $uid:$gid $path{var}
+.
+    }
+
+    my %has_etc;
+    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/schema.*") ) {
+        $has_etc{schema}++;
+    }
+    if ( File::Glob::bsd_glob("$FindBin::Bin/etc/acl.*") ) {
+        $has_etc{acl}++;
+    }
+    if ( -e 'etc/initialdata' ) { $has_etc{initialdata}++; }
+    if ( grep { /\d+\.\d+\.\d+.*$/ } glob('etc/upgrade/*.*.*') ) {
+        $has_etc{upgrade}++;
+    }
+
+    $self->postamble("$postamble\n");
+    if ( $path{lib} ) {
+        $self->makemaker_args( INSTALLSITELIB => $path{'lib'} );
+        $self->makemaker_args( INSTALLARCHLIB => $path{'lib'} );
+        $self->makemaker_args( INSTALLVENDORLIB => $path{'lib'} )
+    } else {
+        $self->makemaker_args( PM => { "" => "" }, );
+    }
+
+    $self->makemaker_args( INSTALLSITEMAN1DIR => "$RT::LocalPath/man/man1" );
+    $self->makemaker_args( INSTALLSITEMAN3DIR => "$RT::LocalPath/man/man3" );
+    $self->makemaker_args( INSTALLSITEARCH => "$RT::LocalPath/man" );
+
+    # INSTALLDIRS=vendor should install manpages into /usr/share/man.
+    # That is the default path in most distributions. Need input from
+    # Redhat, Centos etc.
+    $self->makemaker_args( INSTALLVENDORMAN1DIR => "/usr/share/man/man1" );
+    $self->makemaker_args( INSTALLVENDORMAN3DIR => "/usr/share/man/man3" );
+    $self->makemaker_args( INSTALLVENDORARCH => "/usr/share/man" );
+
+    if (%has_etc) {
+        print "For first-time installation, type 'make initdb'.\n";
+        my $initdb = '';
+        $initdb .= <<"." if $has_etc{schema};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(schema \$(NAME) \$(VERSION)))"
+.
+        $initdb .= <<"." if $has_etc{acl};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(acl \$(NAME) \$(VERSION)))"
+.
+        $initdb .= <<"." if $has_etc{initialdata};
+\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(insert \$(NAME) \$(VERSION)))"
+.
+        $self->postamble("initdb ::\n$initdb\n");
+        $self->postamble("initialize-database ::\n$initdb\n");
+        if ($has_etc{upgrade}) {
+            print "To upgrade from a previous version of this extension, use 'make upgrade-database'\n";
+            my $upgradedb = qq|\t\$(NOECHO) \$(PERL) -Ilib -I"$local_lib_path" -I"$lib_path" -Iinc -MModule::Install::RTx::Runtime -e"RTxDatabase(qw(upgrade \$(NAME) \$(VERSION)))"\n|;
+            $self->postamble("upgrade-database ::\n$upgradedb\n");
+            $self->postamble("upgradedb ::\n$upgradedb\n");
+        }
+    }
+
+}
+
+sub requires_rt {
+    my ($self,$version) = @_;
+
+    _load_rt_handle();
+
+    if ($self->is_admin) {
+        $self->add_metadata("x_requires_rt", $version);
+        my @sorted = sort RT::Handle::cmp_version $version,'4.0.0';
+        $self->perl_version('5.008003') if $sorted[0] eq '4.0.0'
+            and (not $self->perl_version or '5.008003' > $self->perl_version);
+        @sorted = sort RT::Handle::cmp_version $version,'4.2.0';
+        $self->perl_version('5.010001') if $sorted[0] eq '4.2.0'
+            and (not $self->perl_version or '5.010001' > $self->perl_version);
+    }
+
+    # if we're exactly the same version as what we want, silently return
+    return if ($version eq $RT::VERSION);
+
+    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+    if ($sorted[-1] eq $version) {
+        die <<"EOT";
+
+**** Error: This extension requires RT $version. Your installed version
+            of RT ($RT::VERSION) is too old.
+
+EOT
+    }
+}
+
+sub requires_rt_plugin {
+    my $self = shift;
+    my ( $plugin ) = @_;
+
+    if ($self->is_admin) {
+        my $plugins = $self->Meta->{values}{"x_requires_rt_plugins"} || [];
+        push @{$plugins}, $plugin;
+        $self->add_metadata("x_requires_rt_plugins", $plugins);
+    }
+
+    my $path = $plugin;
+    $path =~ s{\:\:}{-}g;
+    $path = "$RT::LocalPluginPath/$path/lib";
+    if ( -e $path ) {
+        unshift @INC, $path;
+    } else {
+        my $name = $self->name;
+        warn <<"EOT";
+
+**** Warning: $name requires that the $plugin plugin be installed and
+              enabled; it does not appear to be installed.
+
+EOT
+    }
+    $self->requires(@_);
+}
+
+sub rt_too_new {
+    my ($self,$version,$msg) = @_;
+    my $name = $self->name;
+    $msg ||= <<EOT;
+
+**** Error: Your installed version of RT (%s) is too new; this extension
+            only works with versions older than %s.
+
+EOT
+    $self->add_metadata("x_rt_too_new", $version) if $self->is_admin;
+
+    _load_rt_handle();
+    my @sorted = sort RT::Handle::cmp_version $version,$RT::VERSION;
+
+    if ($sorted[0] eq $version) {
+        die sprintf($msg,$RT::VERSION,$version);
+    }
+}
+
+# RT::Handle runs FinalizeDatabaseType which calls RT->Config->Get
+# On 3.8, this dies.  On 4.0/4.2 ->Config transparently runs LoadConfig.
+# LoadConfig requires being able to read RT_SiteConfig.pm (root) so we'd
+# like to avoid pushing that on users.
+# Fake up just enough Config to let FinalizeDatabaseType finish, and
+# anyone later calling LoadConfig will overwrite our shenanigans.
+sub _load_rt_handle {
+    unless ($RT::Config) {
+        require RT::Config;
+        $RT::Config = RT::Config->new;
+        RT->Config->Set('DatabaseType','mysql');
+    }
+    require RT::Handle;
+}
+
+1;
+
+__END__
+
+#line 468
diff --git a/inc/Module/Install/RTx/Runtime.pm b/inc/Module/Install/RTx/Runtime.pm
new file mode 100644
index 0000000..937949f
--- /dev/null
+++ b/inc/Module/Install/RTx/Runtime.pm
@@ -0,0 +1,79 @@
+#line 1
+package Module::Install::RTx::Runtime;
+
+use base 'Exporter';
+our @EXPORT = qw/RTxDatabase RTxPlugin/;
+
+use strict;
+use File::Basename ();
+
+sub _rt_runtime_load {
+    require RT;
+
+    eval { RT::LoadConfig(); };
+    if (my $err = $@) {
+        die $err unless $err =~ /^RT couldn't load RT config file/m;
+        my $warn = <<EOT;
+This usually means that your current user cannot read the file.  You
+will likely need to run this installation step as root, or some user
+with more permissions.
+EOT
+        $err =~ s/This usually means.*/$warn/s;
+        die $err;
+    }
+}
+
+sub RTxDatabase {
+    my ($action, $name, $version) = @_;
+
+    _rt_runtime_load();
+
+    require RT::System;
+    my $has_upgrade = RT::System->can('AddUpgradeHistory');
+
+    my $lib_path = File::Basename::dirname($INC{'RT.pm'});
+    my @args = (
+        "-Ilib",
+        "-I$RT::LocalLibPath",
+        "-I$lib_path",
+        "$RT::SbinPath/rt-setup-database",
+        "--action"      => $action,
+        ($action eq 'upgrade' ? () : ("--datadir"     => "etc")),
+        (($action eq 'insert') ? ("--datafile"    => "etc/initialdata") : ()),
+        "--dba"         => $RT::DatabaseAdmin || $RT::DatabaseUser,
+        "--prompt-for-dba-password" => '',
+        ($has_upgrade ? ("--package" => $name, "--ext-version" => $version) : ()),
+    );
+    # If we're upgrading against an RT which isn't at least 4.2 (has
+    # AddUpgradeHistory) then pass --package.  Upgrades against later RT
+    # releases will pick up --package from AddUpgradeHistory.
+    if ($action eq 'upgrade' and not $has_upgrade) {
+        push @args, "--package" => $name;
+    }
+
+    print "$^X @args\n";
+    (system($^X, @args) == 0) or die "...returned with error: $?\n";
+}
+
+sub RTxPlugin {
+    my ($name) = @_;
+
+    _rt_runtime_load();
+    require YAML::Tiny;
+    my $data = YAML::Tiny::LoadFile('META.yml');
+    my $name = $data->{name};
+
+    my @enabled = RT->Config->Get('Plugins');
+    for my $required (@{$data->{x_requires_rt_plugins} || []}) {
+        next if grep {$required eq $_} @enabled;
+
+        warn <<"EOT";
+
+**** Warning: $name requires that the $required plugin be installed and
+              enabled; it is not currently in \@Plugins.
+
+EOT
+    }
+}
+
+1;
diff --git a/inc/Module/Install/ReadmeFromPod.pm b/inc/Module/Install/ReadmeFromPod.pm
new file mode 100644
index 0000000..3738232
--- /dev/null
+++ b/inc/Module/Install/ReadmeFromPod.pm
@@ -0,0 +1,184 @@
+#line 1
+package Module::Install::ReadmeFromPod;
+
+use 5.006;
+use strict;
+use warnings;
+use base qw(Module::Install::Base);
+use vars qw($VERSION);
+
+$VERSION = '0.30';
+
+{
+
+    # these aren't defined until after _require_admin is run, so
+    # define them so prototypes are available during compilation.
+    sub io;
+    sub capture(&;@);
+
+#line 28
+
+    my $done = 0;
+
+    sub _require_admin {
+
+	# do this once to avoid redefinition warnings from IO::All
+	return if $done;
+
+	require IO::All;
+	IO::All->import( '-binary' );
+
+	require Capture::Tiny;
+	Capture::Tiny->import ( 'capture' );
+
+	return;
+    }
+
+}
+
+sub readme_from {
+  my $self = shift;
+  return unless $self->is_admin;
+
+  _require_admin;
+
+  # Input file
+  my $in_file  = shift || $self->_all_from
+    or die "Can't determine file to make readme_from";
+
+  # Get optional arguments
+  my ($clean, $format, $out_file, $options);
+  my $args = shift;
+  if ( ref $args ) {
+    # Arguments are in a hashref
+    if ( ref($args) ne 'HASH' ) {
+      die "Expected a hashref but got a ".ref($args)."\n";
+    } else {
+      $clean    = $args->{'clean'};
+      $format   = $args->{'format'};
+      $out_file = $args->{'output_file'};
+      $options  = $args->{'options'};
+    }
+  } else {
+    # Arguments are in a list
+    $clean    = $args;
+    $format   = shift;
+    $out_file = shift;
+    $options  = \@_;
+  }
+
+  # Default values;
+  $clean  ||= 0;
+  $format ||= 'txt';
+
+  # Generate README
+  print "readme_from $in_file to $format\n";
+  if ($format =~ m/te?xt/) {
+    $out_file = $self->_readme_txt($in_file, $out_file, $options);
+  } elsif ($format =~ m/html?/) {
+    $out_file = $self->_readme_htm($in_file, $out_file, $options);
+  } elsif ($format eq 'man') {
+    $out_file = $self->_readme_man($in_file, $out_file, $options);
+  } elsif ($format eq 'md') {
+    $out_file = $self->_readme_md($in_file, $out_file, $options);
+  } elsif ($format eq 'pdf') {
+    $out_file = $self->_readme_pdf($in_file, $out_file, $options);
+  }
+
+  if ($clean) {
+    $self->clean_files($out_file);
+  }
+
+  return 1;
+}
+
+
+sub _readme_txt {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README';
+  require Pod::Text;
+  my $parser = Pod::Text->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _readme_htm {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.htm';
+  require Pod::Html;
+  my ($o) = capture {
+    Pod::Html::pod2html(
+      "--infile=$in_file",
+      "--outfile=-",
+      @$options,
+    );
+  };
+  io->file($out_file)->print($o);
+  # Remove temporary files if needed
+  for my $file ('pod2htmd.tmp', 'pod2htmi.tmp') {
+    if (-e $file) {
+      unlink $file or warn "Warning: Could not remove file '$file'.\n$!\n";
+    }
+  }
+  return $out_file;
+}
+
+
+sub _readme_man {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.1';
+  require Pod::Man;
+  my $parser = Pod::Man->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _readme_pdf {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.pdf';
+  eval { require App::pod2pdf; }
+    or die "Could not generate $out_file because pod2pdf could not be found\n";
+  my $parser = App::pod2pdf->new( @$options );
+  $parser->parse_from_file($in_file);
+  my ($o) = capture { $parser->output };
+  io->file($out_file)->print($o);
+  return $out_file;
+}
+
+sub _readme_md {
+  my ($self, $in_file, $out_file, $options) = @_;
+  $out_file ||= 'README.md';
+  require Pod::Markdown;
+  my $parser = Pod::Markdown->new( @$options );
+  my $io = io->file($out_file)->open(">");
+  my $out_fh = $io->io_handle;
+  $parser->output_fh( *$out_fh );
+  $parser->parse_file( $in_file );
+  return $out_file;
+}
+
+
+sub _all_from {
+  my $self = shift;
+  return unless $self->admin->{extensions};
+  my ($metadata) = grep {
+    ref($_) eq 'Module::Install::Metadata';
+  } @{$self->admin->{extensions}};
+  return unless $metadata;
+  return $metadata->{values}{all_from} || '';
+}
+
+'Readme!';
+
+__END__
+
+#line 316
+
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
new file mode 100644
index 0000000..eeaa3fe
--- /dev/null
+++ b/inc/Module/Install/Win32.pm
@@ -0,0 +1,64 @@
+#line 1
+package Module::Install::Win32;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = 'Module::Install::Base';
+	$ISCORE  = 1;
+}
+
+# determine if the user needs nmake, and download it if needed
+sub check_nmake {
+	my $self = shift;
+	$self->load('can_run');
+	$self->load('get_file');
+
+	require Config;
+	return unless (
+		$^O eq 'MSWin32'                     and
+		$Config::Config{make}                and
+		$Config::Config{make} =~ /^nmake\b/i and
+		! $self->can_run('nmake')
+	);
+
+	print "The required 'nmake' executable not found, fetching it...\n";
+
+	require File::Basename;
+	my $rv = $self->get_file(
+		url       => 'http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe',
+		ftp_url   => 'ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe',
+		local_dir => File::Basename::dirname($^X),
+		size      => 51928,
+		run       => 'Nmake15.exe /o > nul',
+		check_for => 'Nmake.exe',
+		remove    => 1,
+	);
+
+	die <<'END_MESSAGE' unless $rv;
+
+-------------------------------------------------------------------------------
+
+Since you are using Microsoft Windows, you will need the 'nmake' utility
+before installation. It's available at:
+
+  http://download.microsoft.com/download/vc15/Patch/1.52/W95/EN-US/Nmake15.exe
+      or
+  ftp://ftp.microsoft.com/Softlib/MSLFILES/Nmake15.exe
+
+Please download the file manually, save it to a directory in %PATH% (e.g.
+C:\WINDOWS\COMMAND\), then launch the MS-DOS command line shell, "cd" to
+that directory, and run "Nmake15.exe" from there; that will create the
+'nmake.exe' file needed by this module.
+
+You may then resume the installation process described in README.
+
+-------------------------------------------------------------------------------
+END_MESSAGE
+
+}
+
+1;
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
new file mode 100644
index 0000000..85d8018
--- /dev/null
+++ b/inc/Module/Install/WriteAll.pm
@@ -0,0 +1,63 @@
+#line 1
+package Module::Install::WriteAll;
+
+use strict;
+use Module::Install::Base ();
+
+use vars qw{$VERSION @ISA $ISCORE};
+BEGIN {
+	$VERSION = '1.06';
+	@ISA     = qw{Module::Install::Base};
+	$ISCORE  = 1;
+}
+
+sub WriteAll {
+	my $self = shift;
+	my %args = (
+		meta        => 1,
+		sign        => 0,
+		inline      => 0,
+		check_nmake => 1,
+		@_,
+	);
+
+	$self->sign(1)                if $args{sign};
+	$self->admin->WriteAll(%args) if $self->is_admin;
+
+	$self->check_nmake if $args{check_nmake};
+	unless ( $self->makemaker_args->{PL_FILES} ) {
+		# XXX: This still may be a bit over-defensive...
+		unless ($self->makemaker(6.25)) {
+			$self->makemaker_args( PL_FILES => {} ) if -f 'Build.PL';
+		}
+	}
+
+	# Until ExtUtils::MakeMaker support MYMETA.yml, make sure
+	# we clean it up properly ourself.
+	$self->realclean_files('MYMETA.yml');
+
+	if ( $args{inline} ) {
+		$self->Inline->write;
+	} else {
+		$self->Makefile->write;
+	}
+
+	# The Makefile write process adds a couple of dependencies,
+	# so write the META.yml files after the Makefile.
+	if ( $args{meta} ) {
+		$self->Meta->write;
+	}
+
+	# Experimental support for MYMETA
+	if ( $ENV{X_MYMETA} ) {
+		if ( $ENV{X_MYMETA} eq 'JSON' ) {
+			$self->Meta->write_mymeta_json;
+		} else {
+			$self->Meta->write_mymeta_yaml;
+		}
+	}
+
+	return 1;
+}
+
+1;
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
new file mode 100644
index 0000000..9a4e291
--- /dev/null
+++ b/inc/YAML/Tiny.pm
@@ -0,0 +1,650 @@
+#line 1
+package YAML::Tiny;
+BEGIN {
+  $YAML::Tiny::AUTHORITY = 'cpan:ADAMK';
+}
+{
+  $YAML::Tiny::VERSION = '1.56';
+}
+# git description: v1.55-3-gc945058
+
+
+use strict;
+use warnings;
+
+# UTF Support?
+sub HAVE_UTF8 () { $] >= 5.007003 }
+BEGIN {
+    if ( HAVE_UTF8 ) {
+        # The string eval helps hide this from Test::MinimumVersion
+        eval "require utf8;";
+        die "Failed to load UTF-8 support" if $@;
+    }
+
+    # Class structure
+    require 5.004;
+    require Exporter;
+    require Carp;
+    @YAML::Tiny::ISA       = qw{ Exporter  };
+    @YAML::Tiny::EXPORT    = qw{ Load Dump };
+    @YAML::Tiny::EXPORT_OK = qw{ LoadFile DumpFile freeze thaw };
+
+    # Error storage
+    $YAML::Tiny::errstr    = '';
+}
+
+# The character class of all characters we need to escape
+# NOTE: Inlined, since it's only used once
+# my $RE_ESCAPE = '[\\x00-\\x08\\x0b-\\x0d\\x0e-\\x1f\"\n]';
+
+# Printed form of the unprintable characters in the lowest range
+# of ASCII characters, listed by ASCII ordinal position.
+my @UNPRINTABLE = qw(
+    z    x01  x02  x03  x04  x05  x06  a
+    x08  t    n    v    f    r    x0e  x0f
+    x10  x11  x12  x13  x14  x15  x16  x17
+    x18  x19  x1a  e    x1c  x1d  x1e  x1f
+);
+
+# Printable characters for escapes
+my %UNESCAPES = (
+    z => "\x00", a => "\x07", t    => "\x09",
+    n => "\x0a", v => "\x0b", f    => "\x0c",
+    r => "\x0d", e => "\x1b", '\\' => '\\',
+);
+
+# Special magic boolean words
+my %QUOTE = map { $_ => 1 } qw{
+    null Null NULL
+    y Y yes Yes YES n N no No NO
+    true True TRUE false False FALSE
+    on On ON off Off OFF
+};
+
+
+
+
+
+#####################################################################
+# Implementation
+
+# Create an empty YAML::Tiny object
+sub new {
+    my $class = shift;
+    bless [ @_ ], $class;
+}
+
+# Create an object from a file
+sub read {
+    my $class = ref $_[0] ? ref shift : shift;
+
+    # Check the file
+    my $file = shift or return $class->_error( 'You did not specify a file name' );
+    return $class->_error( "File '$file' does not exist" )              unless -e $file;
+    return $class->_error( "'$file' is a directory, not a file" )       unless -f _;
+    return $class->_error( "Insufficient permissions to read '$file'" ) unless -r _;
+
+    # Slurp in the file
+    local $/ = undef;
+    local *CFG;
+    unless ( open(CFG, $file) ) {
+        return $class->_error("Failed to open file '$file': $!");
+    }
+    my $contents = <CFG>;
+    unless ( close(CFG) ) {
+        return $class->_error("Failed to close file '$file': $!");
+    }
+
+    $class->read_string( $contents );
+}
+
+# Create an object from a string
+sub read_string {
+    my $class  = ref $_[0] ? ref shift : shift;
+    my $self   = bless [], $class;
+    my $string = $_[0];
+    eval {
+        unless ( defined $string ) {
+            die \"Did not provide a string to load";
+        }
+
+        # Byte order marks
+        # NOTE: Keeping this here to educate maintainers
+        # my %BOM = (
+        #     "\357\273\277" => 'UTF-8',
+        #     "\376\377"     => 'UTF-16BE',
+        #     "\377\376"     => 'UTF-16LE',
+        #     "\377\376\0\0" => 'UTF-32LE'
+        #     "\0\0\376\377" => 'UTF-32BE',
+        # );
+        if ( $string =~ /^(?:\376\377|\377\376|\377\376\0\0|\0\0\376\377)/ ) {
+            die \"Stream has a non UTF-8 BOM";
+        } else {
+            # Strip UTF-8 bom if found, we'll just ignore it
+            $string =~ s/^\357\273\277//;
+        }
+
+        # Try to decode as utf8
+        utf8::decode($string) if HAVE_UTF8;
+
+        # Check for some special cases
+        return $self unless length $string;
+        unless ( $string =~ /[\012\015]+\z/ ) {
+            die \"Stream does not end with newline character";
+        }
+
+        # Split the file into lines
+        my @lines = grep { ! /^\s*(?:\#.*)?\z/ }
+                split /(?:\015{1,2}\012|\015|\012)/, $string;
+
+        # Strip the initial YAML header
+        @lines and $lines[0] =~ /^\%YAML[: ][\d\.]+.*\z/ and shift @lines;
+
+        # A nibbling parser
+        while ( @lines ) {
+            # Do we have a document header?
+            if ( $lines[0] =~ /^---\s*(?:(.+)\s*)?\z/ ) {
+                # Handle scalar documents
+                shift @lines;
+                if ( defined $1 and $1 !~ /^(?:\#.+|\%YAML[: ][\d\.]+)\z/ ) {
+                    push @$self, $self->_read_scalar( "$1", [ undef ], \@lines );
+                    next;
+                }
+            }
+
+            if ( ! @lines or $lines[0] =~ /^(?:---|\.\.\.)/ ) {
+                # A naked document
+                push @$self, undef;
+                while ( @lines and $lines[0] !~ /^---/ ) {
+                    shift @lines;
+                }
+
+            } elsif ( $lines[0] =~ /^\s*\-/ ) {
+                # An array at the root
+                my $document = [ ];
+                push @$self, $document;
+                $self->_read_array( $document, [ 0 ], \@lines );
+
+            } elsif ( $lines[0] =~ /^(\s*)\S/ ) {
+                # A hash at the root
+                my $document = { };
+                push @$self, $document;
+                $self->_read_hash( $document, [ length($1) ], \@lines );
+
+            } else {
+                die \"YAML::Tiny failed to classify the line '$lines[0]'";
+            }
+        }
+    };
+    if ( ref $@ eq 'SCALAR' ) {
+        return $self->_error(${$@});
+    } elsif ( $@ ) {
+        require Carp;
+        Carp::croak($@);
+    }
+
+    return $self;
+}
+
+# Deparse a scalar string to the actual scalar
+sub _read_scalar {
+    my ($self, $string, $indent, $lines) = @_;
+
+    # Trim trailing whitespace
+    $string =~ s/\s*\z//;
+
+    # Explitic null/undef
+    return undef if $string eq '~';
+
+    # Single quote
+    if ( $string =~ /^\'(.*?)\'(?:\s+\#.*)?\z/ ) {
+        return '' unless defined $1;
+        $string = $1;
+        $string =~ s/\'\'/\'/g;
+        return $string;
+    }
+
+    # Double quote.
+    # The commented out form is simpler, but overloaded the Perl regex
+    # engine due to recursion and backtracking problems on strings
+    # larger than 32,000ish characters. Keep it for reference purposes.
+    # if ( $string =~ /^\"((?:\\.|[^\"])*)\"\z/ ) {
+    if ( $string =~ /^\"([^\\"]*(?:\\.[^\\"]*)*)\"(?:\s+\#.*)?\z/ ) {
+        # Reusing the variable is a little ugly,
+        # but avoids a new variable and a string copy.
+        $string = $1;
+        $string =~ s/\\"/"/g;
+        $string =~ s/\\([never\\fartz]|x([0-9a-fA-F]{2}))/(length($1)>1)?pack("H2",$2):$UNESCAPES{$1}/gex;
+        return $string;
+    }
+
+    # Special cases
+    if ( $string =~ /^[\'\"!&]/ ) {
+        die \"YAML::Tiny does not support a feature in line '$string'";
+    }
+    return {} if $string =~ /^{}(?:\s+\#.*)?\z/;
+    return [] if $string =~ /^\[\](?:\s+\#.*)?\z/;
+
+    # Regular unquoted string
+    if ( $string !~ /^[>|]/ ) {
+        if (
+            $string =~ /^(?:-(?:\s|$)|[\@\%\`])/
+            or
+            $string =~ /:(?:\s|$)/
+        ) {
+            die \"YAML::Tiny found illegal characters in plain scalar: '$string'";
+        }
+        $string =~ s/\s+#.*\z//;
+        return $string;
+    }
+
+    # Error
+    die \"YAML::Tiny failed to find multi-line scalar content" unless @$lines;
+
+    # Check the indent depth
+    $lines->[0]   =~ /^(\s*)/;
+    $indent->[-1] = length("$1");
+    if ( defined $indent->[-2] and $indent->[-1] <= $indent->[-2] ) {
+        die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+    }
+
+    # Pull the lines
+    my @multiline = ();
+    while ( @$lines ) {
+        $lines->[0] =~ /^(\s*)/;
+        last unless length($1) >= $indent->[-1];
+        push @multiline, substr(shift(@$lines), length($1));
+    }
+
+    my $j = (substr($string, 0, 1) eq '>') ? ' ' : "\n";
+    my $t = (substr($string, 1, 1) eq '-') ? ''  : "\n";
+    return join( $j, @multiline ) . $t;
+}
+
+# Parse an array
+sub _read_array {
+    my ($self, $array, $indent, $lines) = @_;
+
+    while ( @$lines ) {
+        # Check for a new document
+        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
+            while ( @$lines and $lines->[0] !~ /^---/ ) {
+                shift @$lines;
+            }
+            return 1;
+        }
+
+        # Check the indent level
+        $lines->[0] =~ /^(\s*)/;
+        if ( length($1) < $indent->[-1] ) {
+            return 1;
+        } elsif ( length($1) > $indent->[-1] ) {
+            die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+        }
+
+        if ( $lines->[0] =~ /^(\s*\-\s+)[^\'\"]\S*\s*:(?:\s+|$)/ ) {
+            # Inline nested hash
+            my $indent2 = length("$1");
+            $lines->[0] =~ s/-/ /;
+            push @$array, { };
+            $self->_read_hash( $array->[-1], [ @$indent, $indent2 ], $lines );
+
+        } elsif ( $lines->[0] =~ /^\s*\-(\s*)(.+?)\s*\z/ ) {
+            # Array entry with a value
+            shift @$lines;
+            push @$array, $self->_read_scalar( "$2", [ @$indent, undef ], $lines );
+
+        } elsif ( $lines->[0] =~ /^\s*\-\s*\z/ ) {
+            shift @$lines;
+            unless ( @$lines ) {
+                push @$array, undef;
+                return 1;
+            }
+            if ( $lines->[0] =~ /^(\s*)\-/ ) {
+                my $indent2 = length("$1");
+                if ( $indent->[-1] == $indent2 ) {
+                    # Null array entry
+                    push @$array, undef;
+                } else {
+                    # Naked indenter
+                    push @$array, [ ];
+                    $self->_read_array( $array->[-1], [ @$indent, $indent2 ], $lines );
+                }
+
+            } elsif ( $lines->[0] =~ /^(\s*)\S/ ) {
+                push @$array, { };
+                $self->_read_hash( $array->[-1], [ @$indent, length("$1") ], $lines );
+
+            } else {
+                die \"YAML::Tiny failed to classify line '$lines->[0]'";
+            }
+
+        } elsif ( defined $indent->[-2] and $indent->[-1] == $indent->[-2] ) {
+            # This is probably a structure like the following...
+            # ---
+            # foo:
+            # - list
+            # bar: value
+            #
+            # ... so lets return and let the hash parser handle it
+            return 1;
+
+        } else {
+            die \"YAML::Tiny failed to classify line '$lines->[0]'";
+        }
+    }
+
+    return 1;
+}
+
+# Parse an array
+sub _read_hash {
+    my ($self, $hash, $indent, $lines) = @_;
+
+    while ( @$lines ) {
+        # Check for a new document
+        if ( $lines->[0] =~ /^(?:---|\.\.\.)/ ) {
+            while ( @$lines and $lines->[0] !~ /^---/ ) {
+                shift @$lines;
+            }
+            return 1;
+        }
+
+        # Check the indent level
+        $lines->[0] =~ /^(\s*)/;
+        if ( length($1) < $indent->[-1] ) {
+            return 1;
+        } elsif ( length($1) > $indent->[-1] ) {
+            die \"YAML::Tiny found bad indenting in line '$lines->[0]'";
+        }
+
+        # Get the key
+        unless ( $lines->[0] =~ s/^\s*([^\'\" ][^\n]*?)\s*:(\s+(?:\#.*)?|$)// ) {
+            if ( $lines->[0] =~ /^\s*[?\'\"]/ ) {
+                die \"YAML::Tiny does not support a feature in line '$lines->[0]'";
+            }
+            die \"YAML::Tiny failed to classify line '$lines->[0]'";
+        }
+        my $key = $1;
+
+        # Do we have a value?
+        if ( length $lines->[0] ) {
+            # Yes
+            $hash->{$key} = $self->_read_scalar( shift(@$lines), [ @$indent, undef ], $lines );
+        } else {
+            # An indent
+            shift @$lines;
+            unless ( @$lines ) {
+                $hash->{$key} = undef;
+                return 1;
+            }
+            if ( $lines->[0] =~ /^(\s*)-/ ) {
+                $hash->{$key} = [];
+                $self->_read_array( $hash->{$key}, [ @$indent, length($1) ], $lines );
+            } elsif ( $lines->[0] =~ /^(\s*)./ ) {
+                my $indent2 = length("$1");
+                if ( $indent->[-1] >= $indent2 ) {
+                    # Null hash entry
+                    $hash->{$key} = undef;
+                } else {
+                    $hash->{$key} = {};
+                    $self->_read_hash( $hash->{$key}, [ @$indent, length($1) ], $lines );
+                }
+            }
+        }
+    }
+
+    return 1;
+}
+
+# Save an object to a file
+sub write {
+    my $self = shift;
+    my $file = shift or return $self->_error('No file name provided');
+
+    # Write it to the file
+    open( CFG, '>' . $file ) or return $self->_error(
+        "Failed to open file '$file' for writing: $!"
+        );
+    print CFG $self->write_string;
+    close CFG;
+
+    return 1;
+}
+
+# Save an object to a string
+sub write_string {
+    my $self = shift;
+    return '' unless @$self;
+
+    # Iterate over the documents
+    my $indent = 0;
+    my @lines  = ();
+    foreach my $cursor ( @$self ) {
+        push @lines, '---';
+
+        # An empty document
+        if ( ! defined $cursor ) {
+            # Do nothing
+
+        # A scalar document
+        } elsif ( ! ref $cursor ) {
+            $lines[-1] .= ' ' . $self->_write_scalar( $cursor, $indent );
+
+        # A list at the root
+        } elsif ( ref $cursor eq 'ARRAY' ) {
+            unless ( @$cursor ) {
+                $lines[-1] .= ' []';
+                next;
+            }
+            push @lines, $self->_write_array( $cursor, $indent, {} );
+
+        # A hash at the root
+        } elsif ( ref $cursor eq 'HASH' ) {
+            unless ( %$cursor ) {
+                $lines[-1] .= ' {}';
+                next;
+            }
+            push @lines, $self->_write_hash( $cursor, $indent, {} );
+
+        } else {
+            Carp::croak("Cannot serialize " . ref($cursor));
+        }
+    }
+
+    join '', map { "$_\n" } @lines;
+}
+
+sub _write_scalar {
+    my $string = $_[1];
+    return '~'  unless defined $string;
+    return "''" unless length  $string;
+    if ( $string =~ /[\x00-\x08\x0b-\x0d\x0e-\x1f\"\'\n]/ ) {
+        $string =~ s/\\/\\\\/g;
+        $string =~ s/"/\\"/g;
+        $string =~ s/\n/\\n/g;
+        $string =~ s/([\x00-\x1f])/\\$UNPRINTABLE[ord($1)]/g;
+        return qq|"$string"|;
+    }
+    if ( $string =~ /(?:^\W|\s|:\z)/ or $QUOTE{$string} ) {
+        return "'$string'";
+    }
+    return $string;
+}
+
+sub _write_array {
+    my ($self, $array, $indent, $seen) = @_;
+    if ( $seen->{refaddr($array)}++ ) {
+        die "YAML::Tiny does not support circular references";
+    }
+    my @lines  = ();
+    foreach my $el ( @$array ) {
+        my $line = ('  ' x $indent) . '-';
+        my $type = ref $el;
+        if ( ! $type ) {
+            $line .= ' ' . $self->_write_scalar( $el, $indent + 1 );
+            push @lines, $line;
+
+        } elsif ( $type eq 'ARRAY' ) {
+            if ( @$el ) {
+                push @lines, $line;
+                push @lines, $self->_write_array( $el, $indent + 1, $seen );
+            } else {
+                $line .= ' []';
+                push @lines, $line;
+            }
+
+        } elsif ( $type eq 'HASH' ) {
+            if ( keys %$el ) {
+                push @lines, $line;
+                push @lines, $self->_write_hash( $el, $indent + 1, $seen );
+            } else {
+                $line .= ' {}';
+                push @lines, $line;
+            }
+
+        } else {
+            die "YAML::Tiny does not support $type references";
+        }
+    }
+
+    @lines;
+}
+
+sub _write_hash {
+    my ($self, $hash, $indent, $seen) = @_;
+    if ( $seen->{refaddr($hash)}++ ) {
+        die "YAML::Tiny does not support circular references";
+    }
+    my @lines  = ();
+    foreach my $name ( sort keys %$hash ) {
+        my $el   = $hash->{$name};
+        my $line = ('  ' x $indent) . "$name:";
+        my $type = ref $el;
+        if ( ! $type ) {
+            $line .= ' ' . $self->_write_scalar( $el, $indent + 1 );
+            push @lines, $line;
+
+        } elsif ( $type eq 'ARRAY' ) {
+            if ( @$el ) {
+                push @lines, $line;
+                push @lines, $self->_write_array( $el, $indent + 1, $seen );
+            } else {
+                $line .= ' []';
+                push @lines, $line;
+            }
+
+        } elsif ( $type eq 'HASH' ) {
+            if ( keys %$el ) {
+                push @lines, $line;
+                push @lines, $self->_write_hash( $el, $indent + 1, $seen );
+            } else {
+                $line .= ' {}';
+                push @lines, $line;
+            }
+
+        } else {
+            die "YAML::Tiny does not support $type references";
+        }
+    }
+
+    @lines;
+}
+
+# Set error
+sub _error {
+    $YAML::Tiny::errstr = $_[1];
+    undef;
+}
+
+# Retrieve error
+sub errstr {
+    $YAML::Tiny::errstr;
+}
+
+
+
+
+
+#####################################################################
+# YAML Compatibility
+
+sub Dump {
+    YAML::Tiny->new(@_)->write_string;
+}
+
+sub Load {
+    my $self = YAML::Tiny->read_string(@_);
+    unless ( $self ) {
+        Carp::croak("Failed to load YAML document from string");
+    }
+    if ( wantarray ) {
+        return @$self;
+    } else {
+        # To match YAML.pm, return the last document
+        return $self->[-1];
+    }
+}
+
+BEGIN {
+    *freeze = *Dump;
+    *thaw   = *Load;
+}
+
+sub DumpFile {
+    my $file = shift;
+    YAML::Tiny->new(@_)->write($file);
+}
+
+sub LoadFile {
+    my $self = YAML::Tiny->read($_[0]);
+    unless ( $self ) {
+        Carp::croak("Failed to load YAML document from '" . ($_[0] || '') . "'");
+    }
+    if ( wantarray ) {
+        return @$self;
+    } else {
+        # Return only the last document to match YAML.pm,
+        return $self->[-1];
+    }
+}
+
+
+
+
+
+#####################################################################
+# Use Scalar::Util if possible, otherwise emulate it
+
+BEGIN {
+    local $@;
+    eval {
+        require Scalar::Util;
+    };
+    my $v = eval("$Scalar::Util::VERSION") || 0;
+    if ( $@ or $v < 1.18 ) {
+        eval <<'END_PERL';
+# Scalar::Util failed to load or too old
+sub refaddr {
+    my $pkg = ref($_[0]) or return undef;
+    if ( !! UNIVERSAL::can($_[0], 'can') ) {
+        bless $_[0], 'Scalar::Util::Fake';
+    } else {
+        $pkg = undef;
+    }
+    "$_[0]" =~ /0x(\w+)/;
+    my $i = do { local $^W; hex $1 };
+    bless $_[0], $pkg if defined $pkg;
+    $i;
+}
+END_PERL
+    } else {
+        *refaddr = *Scalar::Util::refaddr;
+    }
+}
+
+1;
+
+__END__
+
+#line 1223
diff --git a/inc/unicore/Name.pm b/inc/unicore/Name.pm
new file mode 100644
index 0000000..15e729b
--- /dev/null
+++ b/inc/unicore/Name.pm
@@ -0,0 +1,416 @@
+#line 1
+# !!!!!!!   DO NOT EDIT THIS FILE   !!!!!!!
+# This file is machine-generated by lib/unicore/mktables from the Unicode
+# database, Version 6.2.0.  Any changes made here will be lost!
+
+
+# !!!!!!!   INTERNAL PERL USE ONLY   !!!!!!!
+# This file is for internal use by core Perl only.  The format and even the
+# name or existence of this file are subject to change without notice.  Don't
+# use it directly.
+
+
+package charnames;
+
+# This module contains machine-generated tables and code for the
+# algorithmically-determinable Unicode character names.  The following
+# routines can be used to translate between name and code point and vice versa
+
+{ # Closure
+
+    # Matches legal code point.  4-6 hex numbers, If there are 6, the first
+    # two must be 10; if there are 5, the first must not be a 0.  Written this
+    # way to decrease backtracking.  The first regex allows the code point to
+    # be at the end of a word, but to work properly, the word shouldn't end
+    # with a valid hex character.  The second one won't match a code point at
+    # the end of a word, and doesn't have the run-on issue
+    my $run_on_code_point_re = qr/(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b)/;
+    my $code_point_re = qr/(?^aa:\b(?^aax: (?: 10[0-9A-F]{4} | [1-9A-F][0-9A-F]{4} | [0-9A-F]{4} ) \b))/;
+
+    # In the following hash, the keys are the bases of names which include
+    # the code point in the name, like CJK UNIFIED IDEOGRAPH-4E01.  The value
+    # of each key is another hash which is used to get the low and high ends
+    # for each range of code points that apply to the name.
+    my %names_ending_in_code_point = (
+'CJK COMPATIBILITY IDEOGRAPH' => 
+{
+'high' => 
+[
+64109,
+64217,
+195101,
+],
+'low' => 
+[
+63744,
+64112,
+194560,
+],
+},
+'CJK UNIFIED IDEOGRAPH' => 
+{
+'high' => 
+[
+19893,
+40908,
+173782,
+177972,
+178205,
+],
+'low' => 
+[
+13312,
+19968,
+131072,
+173824,
+177984,
+],
+},
+
+    );
+
+    # The following hash is a copy of the previous one, except is for loose
+    # matching, so each name has blanks and dashes squeezed out
+    my %loose_names_ending_in_code_point = (
+'CJKCOMPATIBILITYIDEOGRAPH' => 
+{
+'high' => 
+[
+64109,
+64217,
+195101,
+],
+'low' => 
+[
+63744,
+64112,
+194560,
+],
+},
+'CJKUNIFIEDIDEOGRAPH' => 
+{
+'high' => 
+[
+19893,
+40908,
+173782,
+177972,
+178205,
+],
+'low' => 
+[
+13312,
+19968,
+131072,
+173824,
+177984,
+],
+},
+
+    );
+
+    # And the following array gives the inverse mapping from code points to
+    # names.  Lowest code points are first
+    my @code_points_ending_in_code_point = (
+
+{
+'high' => 19893,
+'low' => 13312,
+'name' => 'CJK UNIFIED IDEOGRAPH',
+},
+{
+'high' => 40908,
+'low' => 19968,
+'name' => 'CJK UNIFIED IDEOGRAPH',
+},
+{
+'high' => 64109,
+'low' => 63744,
+'name' => 'CJK COMPATIBILITY IDEOGRAPH',
+},
+{
+'high' => 64217,
+'low' => 64112,
+'name' => 'CJK COMPATIBILITY IDEOGRAPH',
+},
+{
+'high' => 173782,
+'low' => 131072,
+'name' => 'CJK UNIFIED IDEOGRAPH',
+},
+{
+'high' => 177972,
+'low' => 173824,
+'name' => 'CJK UNIFIED IDEOGRAPH',
+},
+{
+'high' => 178205,
+'low' => 177984,
+'name' => 'CJK UNIFIED IDEOGRAPH',
+},
+{
+'high' => 195101,
+'low' => 194560,
+'name' => 'CJK COMPATIBILITY IDEOGRAPH',
+},
+,
+
+    );
+
+    # Convert from code point to Jamo short name for use in composing Hangul
+    # syllable names
+    my %Jamo = (
+4352 => 'G',
+4353 => 'GG',
+4354 => 'N',
+4355 => 'D',
+4356 => 'DD',
+4357 => 'R',
+4358 => 'M',
+4359 => 'B',
+4360 => 'BB',
+4361 => 'S',
+4362 => 'SS',
+4363 => '',
+4364 => 'J',
+4365 => 'JJ',
+4366 => 'C',
+4367 => 'K',
+4368 => 'T',
+4369 => 'P',
+4370 => 'H',
+4449 => 'A',
+4450 => 'AE',
+4451 => 'YA',
+4452 => 'YAE',
+4453 => 'EO',
+4454 => 'E',
+4455 => 'YEO',
+4456 => 'YE',
+4457 => 'O',
+4458 => 'WA',
+4459 => 'WAE',
+4460 => 'OE',
+4461 => 'YO',
+4462 => 'U',
+4463 => 'WEO',
+4464 => 'WE',
+4465 => 'WI',
+4466 => 'YU',
+4467 => 'EU',
+4468 => 'YI',
+4469 => 'I',
+4520 => 'G',
+4521 => 'GG',
+4522 => 'GS',
+4523 => 'N',
+4524 => 'NJ',
+4525 => 'NH',
+4526 => 'D',
+4527 => 'L',
+4528 => 'LG',
+4529 => 'LM',
+4530 => 'LB',
+4531 => 'LS',
+4532 => 'LT',
+4533 => 'LP',
+4534 => 'LH',
+4535 => 'M',
+4536 => 'B',
+4537 => 'BS',
+4538 => 'S',
+4539 => 'SS',
+4540 => 'NG',
+4541 => 'J',
+4542 => 'C',
+4543 => 'K',
+4544 => 'T',
+4545 => 'P',
+4546 => 'H',
+
+    );
+
+    # Leading consonant (can be null)
+    my %Jamo_L = (
+'' => 11,
+'B' => 7,
+'BB' => 8,
+'C' => 14,
+'D' => 3,
+'DD' => 4,
+'G' => 0,
+'GG' => 1,
+'H' => 18,
+'J' => 12,
+'JJ' => 13,
+'K' => 15,
+'M' => 6,
+'N' => 2,
+'P' => 17,
+'R' => 5,
+'S' => 9,
+'SS' => 10,
+'T' => 16,
+
+    );
+
+    # Vowel
+    my %Jamo_V = (
+'A' => 0,
+'AE' => 1,
+'E' => 5,
+'EO' => 4,
+'EU' => 18,
+'I' => 20,
+'O' => 8,
+'OE' => 11,
+'U' => 13,
+'WA' => 9,
+'WAE' => 10,
+'WE' => 15,
+'WEO' => 14,
+'WI' => 16,
+'YA' => 2,
+'YAE' => 3,
+'YE' => 7,
+'YEO' => 6,
+'YI' => 19,
+'YO' => 12,
+'YU' => 17,
+
+    );
+
+    # Optional trailing consonant
+    my %Jamo_T = (
+'B' => 17,
+'BS' => 18,
+'C' => 23,
+'D' => 7,
+'G' => 1,
+'GG' => 2,
+'GS' => 3,
+'H' => 27,
+'J' => 22,
+'K' => 24,
+'L' => 8,
+'LB' => 11,
+'LG' => 9,
+'LH' => 15,
+'LM' => 10,
+'LP' => 14,
+'LS' => 12,
+'LT' => 13,
+'M' => 16,
+'N' => 4,
+'NG' => 21,
+'NH' => 6,
+'NJ' => 5,
+'P' => 26,
+'S' => 19,
+'SS' => 20,
+'T' => 25,
+
+    );
+
+    # Computed re that splits up a Hangul name into LVT or LV syllables
+    my $syllable_re = qr/(|B|BB|C|D|DD|G|GG|H|J|JJ|K|M|N|P|R|S|SS|T)(A|AE|E|EO|EU|I|O|OE|U|WA|WAE|WE|WEO|WI|YA|YAE|YE|YEO|YI|YO|YU)(B|BS|C|D|G|GG|GS|H|J|K|L|LB|LG|LH|LM|LP|LS|LT|M|N|NG|NH|NJ|P|S|SS|T)?/;
+
+    my $HANGUL_SYLLABLE = "HANGUL SYLLABLE ";
+    my $loose_HANGUL_SYLLABLE = "HANGULSYLLABLE";
+
+    # These constants names and values were taken from the Unicode standard,
+    # version 5.1, section 3.12.  They are used in conjunction with Hangul
+    # syllables
+    my $SBase = 0xAC00;
+    my $LBase = 0x1100;
+    my $VBase = 0x1161;
+    my $TBase = 0x11A7;
+    my $SCount = 11172;
+    my $LCount = 19;
+    my $VCount = 21;
+    my $TCount = 28;
+    my $NCount = $VCount * $TCount;
+
+    sub name_to_code_point_special {
+        my ($name, $loose) = @_;
+
+        # Returns undef if not one of the specially handled names; otherwise
+        # returns the code point equivalent to the input name
+        # $loose is non-zero if to use loose matching, 'name' in that case
+        # must be input as upper case with all blanks and dashes squeezed out.
+
+        if ((! $loose && $name =~ s/$HANGUL_SYLLABLE//)
+            || ($loose && $name =~ s/$loose_HANGUL_SYLLABLE//))
+        {
+            return if $name !~ qr/^$syllable_re$/;
+            my $L = $Jamo_L{$1};
+            my $V = $Jamo_V{$2};
+            my $T = (defined $3) ? $Jamo_T{$3} : 0;
+            return ($L * $VCount + $V) * $TCount + $T + $SBase;
+        }
+
+        # Name must end in 'code_point' for this to handle.
+        return if (($loose && $name !~ /^ (.*?) ($run_on_code_point_re) $/x)
+                   || (! $loose && $name !~ /^ (.*) ($code_point_re) $/x));
+
+        my $base = $1;
+        my $code_point = CORE::hex $2;
+        my $names_ref;
+
+        if ($loose) {
+            $names_ref = \%loose_names_ending_in_code_point;
+        }
+        else {
+            return if $base !~ s/-$//;
+            $names_ref = \%names_ending_in_code_point;
+        }
+
+        # Name must be one of the ones which has the code point in it.
+        return if ! $names_ref->{$base};
+
+        # Look through the list of ranges that apply to this name to see if
+        # the code point is in one of them.
+        for (my $i = 0; $i < scalar @{$names_ref->{$base}{'low'}}; $i++) {
+            return if $names_ref->{$base}{'low'}->[$i] > $code_point;
+            next if $names_ref->{$base}{'high'}->[$i] < $code_point;
+
+            # Here, the code point is in the range.
+            return $code_point;
+        }
+
+        # Here, looked like the name had a code point number in it, but
+        # did not match one of the valid ones.
+        return;
+    }
+
+    sub code_point_to_name_special {
+        my $code_point = shift;
+
+        # Returns the name of a code point if algorithmically determinable;
+        # undef if not
+
+        # If in the Hangul range, calculate the name based on Unicode's
+        # algorithm
+        if ($code_point >= $SBase && $code_point <= $SBase + $SCount -1) {
+            use integer;
+            my $SIndex = $code_point - $SBase;
+            my $L = $LBase + $SIndex / $NCount;
+            my $V = $VBase + ($SIndex % $NCount) / $TCount;
+            my $T = $TBase + $SIndex % $TCount;
+            $name = "$HANGUL_SYLLABLE$Jamo{$L}$Jamo{$V}";
+            $name .= $Jamo{$T} if $T != $TBase;
+            return $name;
+        }
+
+        # Look through list of these code points for one in range.
+        foreach my $hash (@code_points_ending_in_code_point) {
+            return if $code_point < $hash->{'low'};
+            if ($code_point <= $hash->{'high'}) {
+                return sprintf("%s-%04X", $hash->{'name'}, $code_point);
+            }
+        }
+        return;            # None found
+    }
+} # End closure
+
+1;
diff --git a/lib/RT/Authen/Token.pm b/lib/RT/Authen/Token.pm
new file mode 100644
index 0000000..4f02841
--- /dev/null
+++ b/lib/RT/Authen/Token.pm
@@ -0,0 +1,59 @@
+package RT::Authen::Token;
+use strict;
+use warnings;
+
+our $VERSION = '0.01';
+
+=head1 NAME
+
+RT-Authen-Token - token-based authentication
+
+=head1 INSTALLATION
+
+RT-Authen-Token requires version RT 4.2.0 or later.
+
+=over
+
+=item perl Makefile.PL
+
+=item make
+
+=item make install
+
+This step may require root permissions.
+
+=item Edit your /opt/rt4/etc/RT_SiteConfig.pm
+
+Add this line:
+
+    Plugin( "RT::Authen::Token" );
+
+=item Restart your webserver
+
+=back
+
+=head1 AUTHOR
+
+Best Practical Solutions, LLC E<lt>modules at bestpractical.comE<gt>
+
+=head1 BUGS
+
+All bugs should be reported via email to
+
+    L<bug-RT-Authen-Token at rt.cpan.org|mailto:bug-RT-Authen-Token at rt.cpan.org>
+
+or via the web at
+
+    L<rt.cpan.org|http://rt.cpan.org/Public/Dist/Display.html?Name=RT-Authen-Token>.
+
+=head1 COPYRIGHT
+
+This extension is Copyright (C) 2017 Best Practical Solutions, LLC.
+
+This is free software, licensed under:
+
+  The GNU General Public License, Version 2, June 1991
+
+=cut
+
+1;

commit 1b03f4c1b5cb58e7c169be0f54286714243879da
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 15:38:09 2017 +0000

    Schemas

diff --git a/MANIFEST b/MANIFEST
index 7a4f33f..d644b6a 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,3 +1,8 @@
+etc/acl.Pg
+etc/schema.mysql
+etc/schema.Oracle
+etc/schema.Pg
+etc/schema.SQLite
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
diff --git a/META.yml b/META.yml
index 38e7f3f..9cbbde1 100644
--- a/META.yml
+++ b/META.yml
@@ -16,6 +16,7 @@ meta-spec:
 name: RT-Authen-Token
 no_index:
   directory:
+    - etc
     - inc
 requires:
   perl: 5.8.3
diff --git a/etc/acl.Pg b/etc/acl.Pg
new file mode 100644
index 0000000..28b1d4e
--- /dev/null
+++ b/etc/acl.Pg
@@ -0,0 +1,30 @@
+sub acl {
+    my $dbh = shift;
+
+    my @acls;
+    my @tables = qw (
+        rtxauthtokens_id_seq
+        RTxAuthTokens
+    );
+
+    my $db_user = RT->Config->Get('DatabaseUser');
+
+    my $sequence_right
+        = ( $dbh->{pg_server_version} >= 80200 )
+        ? "USAGE, SELECT, UPDATE"
+        : "SELECT, UPDATE";
+
+    foreach my $table (@tables) {
+        # Tables are upper-case, sequences are lowercase in @tables
+        if ( $table =~ /^[a-z]/ ) {
+            push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+        }
+        else {
+            push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
+        }
+    }
+    return (@acls);
+}
+
+1;
+
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
new file mode 100644
index 0000000..5a17987
--- /dev/null
+++ b/etc/schema.Oracle
@@ -0,0 +1,15 @@
+CREATE SEQUENCE RTxAuthTokens_seq;
+CREATE TABLE RTxAuthTokens (
+    id              NUMBER(11,0)    CONSTRAINT RTxAuthTokens_key PRIMARY KEY,
+    Owner           NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Token           VARCHAR2(256),
+    Description     varchar2(255)   DEFAULT '',
+    LastUsed        DATE,
+    Creator         NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Created         DATE,
+    LastUpdatedBy   NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    LastUpdated     DATE
+);
+
+CREATE INDEX RTxAuthTokensOwner ON RTxAuthTokens (Owner);
+
diff --git a/etc/schema.Pg b/etc/schema.Pg
new file mode 100644
index 0000000..aa4c110
--- /dev/null
+++ b/etc/schema.Pg
@@ -0,0 +1,15 @@
+CREATE SEQUENCE rtxauthtokens_id_seq;
+CREATE TABLE RTxAuthTokens (
+    id                integer                  DEFAULT nextval('rtxauthtokens_id_seq'),
+    Owner             integer         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           integer         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     integer         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE INDEX RTxAuthTokensOwner ON RTxAuthTokens (Owner);
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
new file mode 100644
index 0000000..3b0bb87
--- /dev/null
+++ b/etc/schema.SQLite
@@ -0,0 +1,14 @@
+CREATE TABLE RTxAuthTokens (
+    id                INTEGER PRIMARY KEY,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    collate NOCASE NULL  ,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL
+);
+
+CREATE INDEX RTxAuthTokensOwner on RTxAuthTokens (Owner);
+
diff --git a/etc/schema.mysql b/etc/schema.mysql
new file mode 100644
index 0000000..32da53b
--- /dev/null
+++ b/etc/schema.mysql
@@ -0,0 +1,15 @@
+CREATE TABLE RTxAuthTokens (
+    id                int(11)         NOT NULL AUTO_INCREMENT,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          datetime                 DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           datetime                 DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       datetime                 DEFAULT NULL,
+    PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX RTxAuthTokensOwner ON RTxAuthTokens (Owner);
+

commit 2edf4b550387ae83f32a8eb2b5801aee0d373ad6
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 19:20:17 2017 +0000

    AuthToken and AuthTokens model classes

diff --git a/MANIFEST b/MANIFEST
index d644b6a..114795c 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -18,6 +18,8 @@ inc/Module/Install/WriteAll.pm
 inc/unicore/Name.pm
 inc/YAML/Tiny.pm
 lib/RT/Authen/Token.pm
+lib/RT/AuthToken.pm
+lib/RT/AuthTokens.pm
 Makefile.PL
 MANIFEST			This list of files
 META.yml
diff --git a/lib/RT/AuthToken.pm b/lib/RT/AuthToken.pm
new file mode 100644
index 0000000..b333248
--- /dev/null
+++ b/lib/RT/AuthToken.pm
@@ -0,0 +1,316 @@
+use strict;
+use warnings;
+use 5.10.1;
+
+package RT::AuthToken;
+use base 'RT::Record';
+
+require RT::User;
+use Digest::SHA 'sha512_hex';
+
+=head1 NAME
+
+RT::AuthToken - Represents an authentication token for a user
+
+=cut
+
+=head1 METHODS
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database.  Available
+keys are:
+
+=over 4
+
+=item Owner
+
+The user ID for whom this token will authenticate. If it's not the AuthToken
+object's CurrentUser, then the AdminUsers permission is required.
+
+=item Description
+
+A human-readable description of what this token will be used for.
+
+=back
+
+Returns a tuple of (status, msg) on failure and (id, msg, authstring) on
+success. Note that this is the only time the authstring will be directly
+readable (as it is stored in the database hashed like a password, so use
+this opportunity to capture it.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = (
+        Owner       => undef,
+        Description => '',
+        @_,
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserHasRight('ManageAuthTokens');
+
+    return (0, $self->loc("Owner required"))
+        unless $args{Owner};
+
+    return (0, $self->loc("Permission Denied"))
+        unless $args{Owner} == $self->CurrentUser->Id
+            || $self->CurrentUserHasRight('AdminUsers');
+
+    my $token = $self->_GenerateToken;
+
+    my ( $id, $msg ) = $self->SUPER::Create(
+        Token => $self->_CryptToken($token),
+        map { $_ => $args{$_} } grep {exists $args{$_}}
+            qw(Owner Description),
+    );
+    unless ($id) {
+        return (0, $self->loc("Authentication token create failed: [_1]", $msg));
+    }
+
+    my $authstring = $self->_BuildAuthString($self->Owner, $token);
+
+    return ($id, $self->loc('Authentication token created'), $authstring);
+}
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the AuthToken
+
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+
+    return 0 unless $self->CurrentUserHasRight('ManageAuthTokens');
+
+    return 0 unless $self->__Value('Owner') == $self->CurrentUser->Id
+                 ||  $self->CurrentUserHasRight('AdminUsers');
+
+    return 1;
+}
+
+=head2 SetOwner
+
+Not permitted
+
+=cut
+
+sub SetOwner {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied"));
+}
+
+=head2 SetToken
+
+Not permitted
+
+=cut
+
+sub SetToken {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied"));
+}
+
+=head2 Delete
+
+Checks ACL
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
+    my ($ok, $msg) = $self->SUPER::Delete(@_);
+    return ($ok, $self->loc("Authentication token revoked.")) if $ok;
+    return ($ok, $msg);
+}
+
+=head2 UpdateLastUsed
+
+Sets the "last used" time, without touching "last updated"
+
+=cut
+
+sub UpdateLastUsed {
+    my $self = shift;
+
+    my $now = RT::Date->new( $self->CurrentUser );
+    $now->SetToNow;
+
+    return $self->__Set(
+        Field => 'LastUsed',
+        Value => $now->ISO,
+    );
+}
+
+=head2 ParseAuthString AUTHSTRING
+
+Class method that takes as input an authstring and provides a tuple
+of (user id, token) on success, or the empty list on failure.
+
+=cut
+
+sub ParseAuthString {
+    my $class = shift;
+    my $input = shift;
+
+    my ($version) = $input =~ s/^([0-9]+)-//
+        or return;
+
+    if ($version == 1) {
+        my ($user_id, $token) = $input =~ /^([0-9]+)-([0-9a-f]{32})$/i
+            or return;
+        return ($user_id, $token);
+    }
+
+    return;
+}
+
+=head2 IsToken
+
+Analogous to L<RT::User/IsPassword>, without all of the legacy password
+forms.
+
+=cut
+
+sub IsToken {
+    my $self = shift;
+    my $value = shift;
+
+    my $stored = $self->__Value('Token');
+
+    # If it's a new-style (>= RT 4.0) password, it starts with a '!'
+    my (undef, $method, @rest) = split /!/, $stored;
+    if ($method eq "bcrypt") {
+        return 0 unless $self->_CryptToken_bcrypt($value, @rest) eq $stored;
+        # Upgrade to a larger number of rounds if necessary
+        return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
+    }
+    else {
+        $RT::Logger->warn("Unknown hash method $method");
+        return 0;
+    }
+
+    # We got here by validating successfully, but with a legacy
+    # password form.  Update to the most recent form.
+    $self->_Set(Field => 'Token', Value => $self->_CryptToken($value));
+    return 1;
+}
+
+=head2 LastUsedObj
+
+L</LastUsed> as an L<RT::Date> object.
+
+=cut
+
+sub LastUsedObj {
+    my $self = shift;
+    my $date = RT::Date->new($self->CurrentUser);
+    $date->Set(Format => 'sql', Value => $self->LastUsed);
+    return $date;
+}
+
+=head1 PRIVATE METHODS
+
+Documented for internal use only, do not call these from outside
+RT::AuthToken itself.
+
+=head2 _Set
+
+Checks if the current user can I<ManageAuthTokens> before calling
+C<SUPER::_Set>.
+
+=cut
+
+sub _Set {
+    my $self = shift;
+    my %args = (
+        Field => undef,
+        Value => undef,
+        @_
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserCanSee;
+
+    return $self->SUPER::_Set(@_);
+}
+
+=head2 _Value
+
+Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
+
+=cut
+
+sub _Value {
+    my $self = shift;
+    return unless $self->CurrentUserCanSee;
+    return $self->SUPER::_Value(@_);
+}
+
+=head2 _GenerateToken
+
+Generates an unpredictable auth token
+
+=cut
+
+sub _GenerateToken {
+    my $class = shift;
+    require Time::HiRes;
+
+    my $input = join '',
+                    Time::HiRes::time(), # subsecond-precision time
+                    {},                  # unpredictable memory address
+                    rand();              # RNG
+
+    my $digest = sha512_hex($input);
+
+    return substr($digest, 0, 32);
+}
+
+=head2 _BuildAuthString
+
+Takes a user id and token and provides an authstring for use in place of
+a (username, password) combo.
+
+=cut
+
+sub _BuildAuthString {
+    my $self    = shift;
+    my $version = 1;
+    my $userid  = shift;
+    my $token   = shift;
+
+    return $version . '-' . $userid . '-' . $token;
+}
+
+sub _CryptToken_bcrypt {
+    my $self = shift;
+    return $self->CurrentUser->UserObj->_GeneratePassword_bcrypt(@_);
+}
+
+sub _CryptToken {
+    my $self = shift;
+    return $self->_CryptToken_bcrypt(@_);
+}
+
+sub Table { "RTxAuthTokens" }
+
+sub _CoreAccessible {
+    {
+        id            => { read => 1, type => 'int(11)',        default => '' },
+        Owner         => { read => 1, type => 'int(11)',        default => '0' },
+        Token         => { read => 1, sql_type => 12, length => 256, is_blob => 0, is_numeric => 0, type => 'varchar(256)', default => ''},
+        Description   => { read => 1, type => 'varchar(255)',   default => '',  write => 1 },
+        LastUsed      => { read => 1, type => 'datetime',       default => '',  write => 1 },
+        Creator       => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        Created       => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+        LastUpdatedBy => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        LastUpdated   => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+    }
+}
+
+1;
diff --git a/lib/RT/AuthTokens.pm b/lib/RT/AuthTokens.pm
new file mode 100644
index 0000000..8909ec7
--- /dev/null
+++ b/lib/RT/AuthTokens.pm
@@ -0,0 +1,51 @@
+use strict;
+use warnings;
+
+package RT::AuthTokens;
+use base 'RT::SearchBuilder';
+
+=head1 NAME
+
+RT::AuthTokens - a collection of L<RT::AuthToken> objects
+
+=cut
+
+=head2 LimitOwner
+
+Limit Owner
+
+=cut
+
+sub LimitOwner {
+    my $self = shift;
+    my %args = (
+        FIELD    => 'Owner',
+        OPERATOR => '=',
+        @_
+    );
+
+    $self->SUPER::Limit(%args);
+}
+
+sub NewItem {
+    my $self = shift;
+    return RT::AuthToken->new( $self->CurrentUser );
+}
+
+=head2 _Init
+
+Sets default ordering by id ascending.
+
+=cut
+
+sub _Init {
+    my $self = shift;
+
+    $self->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+    return $self->SUPER::_Init( @_ );
+}
+
+sub Table { "RTxAuthTokens" }
+
+1;
+
diff --git a/lib/RT/Authen/Token.pm b/lib/RT/Authen/Token.pm
index 4f02841..23056c1 100644
--- a/lib/RT/Authen/Token.pm
+++ b/lib/RT/Authen/Token.pm
@@ -4,6 +4,11 @@ use warnings;
 
 our $VERSION = '0.01';
 
+RT::System->AddRight(Staff => ManageAuthTokens => 'Manage authentication tokens');
+
+use RT::AuthToken;
+use RT::AuthTokens;
+
 =head1 NAME
 
 RT-Authen-Token - token-based authentication

commit 54b478f8ac17850be4198307fbe911a89c69bd38
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 16:04:04 2017 +0000

    Web template scaffolding for managing auth tokens

diff --git a/MANIFEST b/MANIFEST
index 114795c..f1c6eb4 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -3,6 +3,9 @@ etc/schema.mysql
 etc/schema.Oracle
 etc/schema.Pg
 etc/schema.SQLite
+html/Admin/Users/AuthTokens.html
+html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
+html/Prefs/AuthTokens.html
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
 inc/Module/Install/Can.pm
diff --git a/META.yml b/META.yml
index 9cbbde1..fded95d 100644
--- a/META.yml
+++ b/META.yml
@@ -17,6 +17,7 @@ name: RT-Authen-Token
 no_index:
   directory:
     - etc
+    - html
     - inc
 requires:
   perl: 5.8.3
diff --git a/html/Admin/Users/AuthTokens.html b/html/Admin/Users/AuthTokens.html
new file mode 100644
index 0000000..c2c5c72
--- /dev/null
+++ b/html/Admin/Users/AuthTokens.html
@@ -0,0 +1,17 @@
+<& /Admin/Elements/Header, Title => loc("[_1]'s authentication tokens",$UserObj->Name)  &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<%ARGS>
+$id => undef
+</%ARGS>
+<%INIT>
+my @results;
+
+my $UserObj = RT::User->new( $session{'CurrentUser'} );
+$UserObj->Load( $id );
+unless ( $UserObj->id ) {
+    Abort( loc("Couldn't load user #[_1]", $id) );
+}
+$id = $ARGS{'id'} = $UserObj->id;
+</%INIT>
diff --git a/html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged b/html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
new file mode 100644
index 0000000..10df06f
--- /dev/null
+++ b/html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
@@ -0,0 +1,28 @@
+<%INIT>
+return unless $session{'CurrentUser'}->HasRight( Right => 'ManageAuthTokens', Object => RT->System );
+
+if (my $prefs = Menu->child('preferences')) {
+    if (my $settings = $prefs->child('settings')) {
+        $settings->child('about_me')->add_after(auth_tokens => title => loc('Auth Tokens'), path => '/Prefs/AuthTokens.html');
+    }
+}
+
+my $admin = Menu->child('admin')
+    or return;
+
+my $request_path = $HTML::Mason::Commands::r->path_info;
+$request_path =~ s!/{2,}!/!g;
+if ( $request_path =~ m{^(/Admin/Users|/User/(Summary|History)\.html)} and $admin->child("users") ) {
+    if ( $DECODED_ARGS->{'id'} && $DECODED_ARGS->{'id'} =~ /^\d+$/ ) {
+        my $id = $DECODED_ARGS->{'id'};
+        my $obj = RT::User->new( $session{'CurrentUser'} );
+        $obj->Load($id);
+
+        if ( $obj and $obj->id ) {
+            my $tabs = PageMenu();
+            $tabs->child(auth_tokens => title => loc('Auth Tokens'), path => '/Admin/Users/AuthTokens.html?id=' . $id);
+        }
+    }
+}
+
+</%INIT>
diff --git a/html/Prefs/AuthTokens.html b/html/Prefs/AuthTokens.html
new file mode 100644
index 0000000..8e15724
--- /dev/null
+++ b/html/Prefs/AuthTokens.html
@@ -0,0 +1,8 @@
+<& /Elements/Header, Title => loc('My authentication tokens') &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<%INIT>
+my @results;
+my $UserObj = $session{'CurrentUser'}->UserObj;
+</%INIT>

commit 2a6539601bbed4c2fcbe1635467092cdf6f82a8f
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 19:29:55 2017 +0000

    Allow logging in using an authentication token
    
    Note that this is specifically for login. Providing an auth token is not
    enough to, say, change someone else's password

diff --git a/MANIFEST b/MANIFEST
index f1c6eb4..01009ad 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -4,6 +4,7 @@ etc/schema.Oracle
 etc/schema.Pg
 etc/schema.SQLite
 html/Admin/Users/AuthTokens.html
+html/Callbacks/RT-Authen-Token/autohandler/Session
 html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
 html/Prefs/AuthTokens.html
 inc/Module/Install.pm
diff --git a/html/Callbacks/RT-Authen-Token/autohandler/Session b/html/Callbacks/RT-Authen-Token/autohandler/Session
new file mode 100644
index 0000000..6fcc965
--- /dev/null
+++ b/html/Callbacks/RT-Authen-Token/autohandler/Session
@@ -0,0 +1,53 @@
+<%ARGS>
+$user => ''
+$pass => ''
+</%ARGS>
+<%INIT>
+return if RT::Interface::Web::_UserLoggedIn();
+return unless defined $pass;
+
+my ($user_id, $cleartext_token) = RT::AuthToken->ParseAuthString($pass);
+return unless $user_id;
+
+my $user_obj = RT::CurrentUser->new;
+$user_obj->Load($user_id);
+return if !$user_obj->Id || $user_obj->Disabled;
+
+if (length $user) {
+    my $check_user = RT::CurrentUser->new;
+    $check_user->Load($user);
+    return unless $check_user->Id && $user_obj->Id == $check_user->Id;
+}
+
+my $tokens = RT::AuthTokens->new(RT->SystemUser);
+$tokens->LimitOwner(VALUE => $user_id);
+while (my $token = $tokens->Next) {
+    if ($token->IsToken($cleartext_token)) {
+        $token->UpdateLastUsed;
+
+        # log in
+        my $remote_addr = RT::Interface::Web::RequestENV('REMOTE_ADDR');
+        $RT::Logger->info("Successful login for @{[$user_obj->Name]} from $remote_addr using authentication token #@{[$token->Id]} (\"@{[$token->Description]}\")");
+
+        # It's important to nab the next page from the session before we blow
+        # the session away
+        my $next = RT::Interface::Web::RemoveNextPage($ARGS{'next'});
+           $next = $next->{'url'} if ref $next;
+
+        RT::Interface::Web::InstantiateNewSession();
+        $session{'CurrentUser'} = $user_obj;
+
+        # Really the only time we don't want to redirect here is if we were
+        # passed user and pass as query params in the URL.
+        if ($next) {
+            RT::Interface::Web::Redirect($next);
+        }
+        elsif ($ARGS{'next'}) {
+            # Invalid hash, but still wants to go somewhere, take them to /
+            RT::Interface::Web::Redirect(RT->Config->Get('WebURL'));
+        }
+
+        return;
+    }
+}
+</%INIT>

commit 085f295d860637576dbce1f6670246166efd0ced
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 19:31:35 2017 +0000

    Web UI creating new auth tokens

diff --git a/MANIFEST b/MANIFEST
index 01009ad..1af18c7 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -6,6 +6,10 @@ etc/schema.SQLite
 html/Admin/Users/AuthTokens.html
 html/Callbacks/RT-Authen-Token/autohandler/Session
 html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
+html/Elements/AuthToken/CreateButton
+html/Elements/AuthToken/CreateForm
+html/Elements/AuthToken/CreateResults
+html/Helpers/AuthToken/Create
 html/Prefs/AuthTokens.html
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
@@ -28,3 +32,6 @@ Makefile.PL
 MANIFEST			This list of files
 META.yml
 README
+static/css/rt-authen-token.css
+static/images/loading.gif
+static/js/rt-authen-token.js
diff --git a/META.yml b/META.yml
index fded95d..f2036c6 100644
--- a/META.yml
+++ b/META.yml
@@ -19,6 +19,7 @@ no_index:
     - etc
     - html
     - inc
+    - static
 requires:
   perl: 5.8.3
 resources:
diff --git a/html/Admin/Users/AuthTokens.html b/html/Admin/Users/AuthTokens.html
index c2c5c72..1bee79a 100644
--- a/html/Admin/Users/AuthTokens.html
+++ b/html/Admin/Users/AuthTokens.html
@@ -2,6 +2,8 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<& /Elements/AuthToken/CreateButton, Owner => $UserObj->Id &>
+
 <%ARGS>
 $id => undef
 </%ARGS>
diff --git a/html/Elements/AuthToken/CreateButton b/html/Elements/AuthToken/CreateButton
new file mode 100644
index 0000000..f9b54ae
--- /dev/null
+++ b/html/Elements/AuthToken/CreateButton
@@ -0,0 +1,9 @@
+<%ARGS>
+$Owner
+</%ARGS>
+<%INIT>
+</%INIT>
+<div class="authtoken-form-container">
+  <& /Elements/AuthToken/CreateForm, Owner => $Owner &>
+</div>
+<button type="submit" class="authtoken-create">Create Auth Token</button>
diff --git a/html/Elements/AuthToken/CreateForm b/html/Elements/AuthToken/CreateForm
new file mode 100644
index 0000000..39f9642
--- /dev/null
+++ b/html/Elements/AuthToken/CreateForm
@@ -0,0 +1,25 @@
+<%ARGS>
+$Owner
+$Error => ''
+$Description => ''
+</%ARGS>
+<%INIT>
+</%INIT>
+<form class="authtoken-form" method="post" data-ajax-url="<% RT->Config->Get('WebPath') %>/Helpers/AuthToken/Create">
+% if ($Error) {
+<p class="error"><% $Error %></p>
+% }
+<input type="hidden" name="Owner" value="<% $Owner %>">
+<table>
+<tr>
+<td class="label"><&|/l, $session{'CurrentUser'}->Name()&>[_1]'s current password</&>:</td>
+<td class="value"><input type="password" name="Password" size="16" autocomplete="off" /></td>
+</tr>
+<tr>
+<td class="label"><&|/l&>Description</&>:<br><em><&|/l&>What's this token for?</&></em></td>
+<td class="value"><input type="text" name="Description" value="<% $Description %>" size="16" /></td>
+</tr>
+</table>
+<& /Elements/Submit, Label => loc("Create"), Name => 'CreateToken' &>
+<span class="loading"><img src="<%RT->Config->Get('WebPath')%>/static/images/loading.gif" alt="<%loc('Loading')%>" title="<%loc('Loading')%>" /></span>
+</form>
diff --git a/html/Elements/AuthToken/CreateResults b/html/Elements/AuthToken/CreateResults
new file mode 100644
index 0000000..1fd42d4
--- /dev/null
+++ b/html/Elements/AuthToken/CreateResults
@@ -0,0 +1,38 @@
+<%ARGS>
+$Owner => undef
+$Password => ''
+$Description => ''
+</%ARGS>
+<%INIT>
+my $token = RT::AuthToken->new($session{CurrentUser});
+my ($error, $authstring);
+
+if (!$Owner) {
+    $error = loc("Owner required. Please refresh the page and try again.");
+}
+elsif (!length($Description)) {
+    $error = loc("Description cannot be blank.");
+}
+elsif (!length($Password)) {
+    $error = loc("Please enter your current password.");
+}
+elsif (!$session{CurrentUser}->IsPassword($Password) ) {
+    $error = loc("Please enter your current password correctly.");
+}
+else {
+    ((my $ok), (my $msg), $authstring) = $token->Create(
+        Owner       => $Owner,
+        Description => $Description,
+    );
+}
+</%INIT>
+% if ($error) {
+<& /Elements/AuthToken/CreateForm, Owner => $Owner, Error => $error, Description => $Description &>
+% } else {
+<div class="authtoken-success">
+<p><&|/l, $Description&>This is your new authentication token. Treat it carefully like a password. Please save it now because you cannot access it again.</&></p>
+<br>
+<span class="authstring"><% $authstring %></span>
+</div>
+% }
+
diff --git a/html/Helpers/AuthToken/Create b/html/Helpers/AuthToken/Create
new file mode 100644
index 0000000..c57eafc
--- /dev/null
+++ b/html/Helpers/AuthToken/Create
@@ -0,0 +1,2 @@
+<& /Elements/AuthToken/CreateResults, %ARGS &>
+% $m->abort;
diff --git a/html/Prefs/AuthTokens.html b/html/Prefs/AuthTokens.html
index 8e15724..f117c80 100644
--- a/html/Prefs/AuthTokens.html
+++ b/html/Prefs/AuthTokens.html
@@ -2,6 +2,8 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<& /Elements/AuthToken/CreateButton, Owner => $UserObj->id &>
+
 <%INIT>
 my @results;
 my $UserObj = $session{'CurrentUser'}->UserObj;
diff --git a/lib/RT/Authen/Token.pm b/lib/RT/Authen/Token.pm
index 23056c1..137c5a6 100644
--- a/lib/RT/Authen/Token.pm
+++ b/lib/RT/Authen/Token.pm
@@ -9,6 +9,9 @@ RT::System->AddRight(Staff => ManageAuthTokens => 'Manage authentication tokens'
 use RT::AuthToken;
 use RT::AuthTokens;
 
+RT->AddStyleSheets("rt-authen-token.css");
+RT->AddJavaScript("rt-authen-token.js");
+
 =head1 NAME
 
 RT-Authen-Token - token-based authentication
diff --git a/static/css/rt-authen-token.css b/static/css/rt-authen-token.css
new file mode 100644
index 0000000..9dd8cb5
--- /dev/null
+++ b/static/css/rt-authen-token.css
@@ -0,0 +1,33 @@
+.authtoken-form-container {
+    display: none;
+}
+
+.authtoken-form .loading {
+    float: right;
+    display: none;
+}
+
+.authtoken-form.submitting .buttons {
+    display: none;
+}
+
+.authtoken-form.submitting .loading {
+    display: inline;
+}
+
+.authtoken-form .error {
+    color: red;
+}
+
+.authstring {
+    font-size: 1.2em;
+    font-family: monospace;
+    padding: .3em;
+    border: 1px dashed black;
+    background-color: #f9f9f9;
+}
+
+.authtoken-success {
+    margin-bottom: 15px;
+}
+
diff --git a/static/images/loading.gif b/static/images/loading.gif
new file mode 100644
index 0000000..3288d10
Binary files /dev/null and b/static/images/loading.gif differ
diff --git a/static/js/rt-authen-token.js b/static/js/rt-authen-token.js
new file mode 100644
index 0000000..d8de590
--- /dev/null
+++ b/static/js/rt-authen-token.js
@@ -0,0 +1,60 @@
+jQuery(function() {
+    var showModal = function(html) {
+        jQuery("<div class='modal'></div>")
+            .append(html).appendTo("body")
+            .bind('modal:close', function(ev,modal) { modal.elm.remove(); })
+            .modal();
+    };
+
+    jQuery('.authtoken-create').click(function(e) {
+        e.preventDefault();
+        showModal(jQuery('.authtoken-form-container').html());
+    });
+
+    var submitForm = function (form, extraParams) {
+        var payload = form.serializeArray();
+        if (extraParams) {
+            Array.prototype.push.apply(payload, extraParams);
+        }
+
+        form.addClass('submitting');
+        form.find('input').attr('disabled', true);
+
+        var renderResult = function(html) {
+            var form = jQuery('.modal .authtoken-form');
+            if (form.length) {
+                form.replaceWith(html);
+            }
+            else {
+                jQuery('#body').append(html);
+            }
+        };
+
+        jQuery.ajax({
+            method: 'POST',
+            url: form.data('ajax-url'),
+            data: payload,
+            timeout: 30000, /* 30 seconds */
+            success: function (data, status) {
+                renderResult(data);
+            },
+            error: function (xhr, status, error) {
+                renderResult("<p>An error has occurred. Please refresh the page and try again.<p>");
+            }
+        });
+    };
+
+    jQuery('body').on('click', '.authtoken-form button, .authtoken-form input[type=submit]', function (e) {
+        e.preventDefault();
+        var button = jQuery(this);
+
+        var params = [{ name: button.attr('name'), value: button.attr('value') }];
+        submitForm(button.closest('form'), params);
+    });
+
+    jQuery('body').on('submit', '.authtoken-form', function (e) {
+        e.preventDefault();
+        submitForm(jQuery(this));
+    });
+});
+

commit 3d50e4936203d1f17b511a875edf034c91586c99
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 19:52:48 2017 +0000

    Web UI for listing auth tokens

diff --git a/MANIFEST b/MANIFEST
index 1af18c7..2ccd2b2 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -9,6 +9,7 @@ html/Callbacks/RT-Authen-Token/Elements/Tabs/Privileged
 html/Elements/AuthToken/CreateButton
 html/Elements/AuthToken/CreateForm
 html/Elements/AuthToken/CreateResults
+html/Elements/AuthToken/List
 html/Helpers/AuthToken/Create
 html/Prefs/AuthTokens.html
 inc/Module/Install.pm
diff --git a/html/Admin/Users/AuthTokens.html b/html/Admin/Users/AuthTokens.html
index 1bee79a..a6c9f90 100644
--- a/html/Admin/Users/AuthTokens.html
+++ b/html/Admin/Users/AuthTokens.html
@@ -2,7 +2,13 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
+<div class="auth-tokens">
+<p><&|/l&>Authentication tokens allow other applications to use your user account without having to share your password, while allowing you to revoke access on an application-specific basis. Changing your password <em>does not</em> invalidate your auth tokens; you must revoke them here.</&></p>
+<br>
+
 <& /Elements/AuthToken/CreateButton, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/List, Owner => $UserObj->Id &>
+</div>
 
 <%ARGS>
 $id => undef
diff --git a/html/Elements/AuthToken/List b/html/Elements/AuthToken/List
new file mode 100644
index 0000000..094b53b
--- /dev/null
+++ b/html/Elements/AuthToken/List
@@ -0,0 +1,28 @@
+<%ARGS>
+$Owner
+</%ARGS>
+<%INIT>
+my $tokens = RT::AuthTokens->new($session{CurrentUser});
+$tokens->LimitOwner(VALUE => $Owner);
+</%INIT>
+<div class="authtoken-list">
+% if ($tokens->Count == 0) {
+  <em><&|/l&>No authentication tokens.</&></em>
+% } else {
+  <ul>
+% while (my $token = $tokens->Next) {
+    <li id="token-<% $token->Id %>">
+      <span class="description"><% $token->Description %></span>
+      <span class="lastused">
+% my $used = $token->LastUsedObj;
+% if ($used->IsSet) {
+       <&|/l, $used->AgeAsString &>used [_1]</&>
+% } else {
+        <&|/l&>never used</&>
+% }
+      </span>
+    </li>
+% }
+  </ul>
+% }
+</div>
diff --git a/html/Prefs/AuthTokens.html b/html/Prefs/AuthTokens.html
index f117c80..287b47e 100644
--- a/html/Prefs/AuthTokens.html
+++ b/html/Prefs/AuthTokens.html
@@ -2,7 +2,13 @@
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<& /Elements/AuthToken/CreateButton, Owner => $UserObj->id &>
+<div class="auth-tokens">
+<p><&|/l&>Authentication tokens allow other applications to use your user account without having to share your password, while allowing you to revoke access on an application-specific basis. Changing your password <em>does not</em> invalidate your auth tokens; you must revoke them here.</&></p>
+<br>
+
+<& /Elements/AuthToken/CreateButton, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/List, Owner => $UserObj->Id &>
+</div>
 
 <%INIT>
 my @results;
diff --git a/static/css/rt-authen-token.css b/static/css/rt-authen-token.css
index 9dd8cb5..fe767a0 100644
--- a/static/css/rt-authen-token.css
+++ b/static/css/rt-authen-token.css
@@ -31,3 +31,20 @@
     margin-bottom: 15px;
 }
 
+.authtoken-list ul {
+    list-style-type: none;
+    padding-left: 0;
+}
+
+.authtoken-list ul li + li {
+    margin-top: 1em;
+}
+
+.authtoken-list .description {
+    font-weight: bold;
+}
+
+.authtoken-list .lastused {
+    font-style: italic;
+    color: #666;
+}

commit ac78f0b0e87e99fb1bef4aec88d1751eb0983db4
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 21:10:26 2017 +0000

    Refresh list of tokens under the modal on create
    
    Otherwise it looks like the one you just created is missing

diff --git a/MANIFEST b/MANIFEST
index 2ccd2b2..f0ba1a9 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -11,6 +11,7 @@ html/Elements/AuthToken/CreateForm
 html/Elements/AuthToken/CreateResults
 html/Elements/AuthToken/List
 html/Helpers/AuthToken/Create
+html/Helpers/AuthToken/List
 html/Prefs/AuthTokens.html
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
diff --git a/html/Elements/AuthToken/List b/html/Elements/AuthToken/List
index 094b53b..6c82398 100644
--- a/html/Elements/AuthToken/List
+++ b/html/Elements/AuthToken/List
@@ -5,7 +5,9 @@ $Owner
 my $tokens = RT::AuthTokens->new($session{CurrentUser});
 $tokens->LimitOwner(VALUE => $Owner);
 </%INIT>
-<div class="authtoken-list">
+<div class="authtoken-list" data-owner="<% $Owner %>">
+  <span class="loading"><img src="<%RT->Config->Get('WebPath')%>/static/images/loading.gif" alt="<%loc('Loading')%>" title="<%loc('Loading')%>" /></span>
+
 % if ($tokens->Count == 0) {
   <em><&|/l&>No authentication tokens.</&></em>
 % } else {
diff --git a/html/Helpers/AuthToken/List b/html/Helpers/AuthToken/List
new file mode 100644
index 0000000..19ba8a7
--- /dev/null
+++ b/html/Helpers/AuthToken/List
@@ -0,0 +1,2 @@
+<& /Elements/AuthToken/List, Owner => $ARGS{owner} &>
+% $m->abort;
diff --git a/static/css/rt-authen-token.css b/static/css/rt-authen-token.css
index fe767a0..72d489a 100644
--- a/static/css/rt-authen-token.css
+++ b/static/css/rt-authen-token.css
@@ -48,3 +48,14 @@
     font-style: italic;
     color: #666;
 }
+
+.authtoken-list .loading {
+    display: none;
+}
+
+.authtoken-list.refreshing {
+    opacity: 0.3;
+}
+.authtoken-list.refreshing .loading {
+    display: inline;
+}
diff --git a/static/js/rt-authen-token.js b/static/js/rt-authen-token.js
index d8de590..a4b65a1 100644
--- a/static/js/rt-authen-token.js
+++ b/static/js/rt-authen-token.js
@@ -11,6 +11,17 @@ jQuery(function() {
         showModal(jQuery('.authtoken-form-container').html());
     });
 
+    var refreshTokenList = function () {
+        var list = jQuery('.authtoken-list');
+        jQuery.post(
+            RT.Config.WebHomePath + "/Helpers/AuthToken/List",
+            list.data(),
+            function (data) {
+                list.replaceWith(data);
+            }
+        );
+    };
+
     var submitForm = function (form, extraParams) {
         var payload = form.serializeArray();
         if (extraParams) {
@@ -28,6 +39,7 @@ jQuery(function() {
             else {
                 jQuery('#body').append(html);
             }
+            refreshTokenList();
         };
 
         jQuery.ajax({

commit 7d5f812f20fb94659866a639ae583e0085c402df
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 20:08:21 2017 +0000

    Fallback path for when JS is disabled

diff --git a/html/Admin/Users/AuthTokens.html b/html/Admin/Users/AuthTokens.html
index a6c9f90..a310cec 100644
--- a/html/Admin/Users/AuthTokens.html
+++ b/html/Admin/Users/AuthTokens.html
@@ -6,8 +6,8 @@
 <p><&|/l&>Authentication tokens allow other applications to use your user account without having to share your password, while allowing you to revoke access on an application-specific basis. Changing your password <em>does not</em> invalidate your auth tokens; you must revoke them here.</&></p>
 <br>
 
-<& /Elements/AuthToken/CreateButton, Owner => $UserObj->Id &>
-<& /Elements/AuthToken/List, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/CreateButton, %ARGS, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/List, %ARGS, Owner => $UserObj->Id &>
 </div>
 
 <%ARGS>
diff --git a/html/Elements/AuthToken/CreateButton b/html/Elements/AuthToken/CreateButton
index f9b54ae..8f28e3c 100644
--- a/html/Elements/AuthToken/CreateButton
+++ b/html/Elements/AuthToken/CreateButton
@@ -1,9 +1,25 @@
 <%ARGS>
 $Owner
+$ShowCreateForm => 0
+$CreateToken => 0
 </%ARGS>
 <%INIT>
 </%INIT>
+% if ($CreateToken) {
+  <&| /Widgets/TitleBox, title => loc("Create Auth Token") &>
+    <& /Elements/AuthToken/CreateResults, %ARGS &>
+  </&>
+% } elsif ($ShowCreateForm) {
+  <&| /Widgets/TitleBox, title => loc("Create Auth Token") &>
+    <& /Elements/AuthToken/CreateForm, Owner => $Owner &>
+  </&>
+% } else {
 <div class="authtoken-form-container">
   <& /Elements/AuthToken/CreateForm, Owner => $Owner &>
 </div>
-<button type="submit" class="authtoken-create">Create Auth Token</button>
+<form method="GET">
+  <input type="hidden" name="ShowCreateForm" value="1">
+  <input type="hidden" name="id" value="<% $Owner %>">
+  <button type="submit" class="authtoken-create">Create Auth Token</button>
+</form>
+% }
diff --git a/html/Prefs/AuthTokens.html b/html/Prefs/AuthTokens.html
index 287b47e..13f3ec9 100644
--- a/html/Prefs/AuthTokens.html
+++ b/html/Prefs/AuthTokens.html
@@ -6,8 +6,8 @@
 <p><&|/l&>Authentication tokens allow other applications to use your user account without having to share your password, while allowing you to revoke access on an application-specific basis. Changing your password <em>does not</em> invalidate your auth tokens; you must revoke them here.</&></p>
 <br>
 
-<& /Elements/AuthToken/CreateButton, Owner => $UserObj->Id &>
-<& /Elements/AuthToken/List, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/CreateButton, %ARGS, Owner => $UserObj->Id &>
+<& /Elements/AuthToken/List, %ARGS, Owner => $UserObj->Id &>
 </div>
 
 <%INIT>

commit 4b2e2a36c124adaef299aadc8765a221f45847a0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 21:02:17 2017 +0000

    Web UI for modifying tokens

diff --git a/MANIFEST b/MANIFEST
index f0ba1a9..f23982a 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -10,8 +10,11 @@ html/Elements/AuthToken/CreateButton
 html/Elements/AuthToken/CreateForm
 html/Elements/AuthToken/CreateResults
 html/Elements/AuthToken/List
+html/Elements/AuthToken/ModifyForm
+html/Elements/AuthToken/ModifyResults
 html/Helpers/AuthToken/Create
 html/Helpers/AuthToken/List
+html/Helpers/AuthToken/Modify
 html/Prefs/AuthTokens.html
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
diff --git a/html/Elements/AuthToken/List b/html/Elements/AuthToken/List
index 6c82398..d813af2 100644
--- a/html/Elements/AuthToken/List
+++ b/html/Elements/AuthToken/List
@@ -14,6 +14,15 @@ $tokens->LimitOwner(VALUE => $Owner);
   <ul>
 % while (my $token = $tokens->Next) {
     <li id="token-<% $token->Id %>">
+% if ($ARGS{ShowModifyForm} && $ARGS{Token} == $token->Id) {
+      <&| /Widgets/TitleBox, title => loc("Update Auth Token") &>
+% if ($ARGS{Update} || $ARGS{Revoke}) {
+        <& /Elements/AuthToken/ModifyResults, %ARGS, Token => $token->Id &>
+% } else {
+        <& /Elements/AuthToken/ModifyForm, %ARGS, TokenObj => $token &>
+% }
+      </&>
+% } else {
       <span class="description"><% $token->Description %></span>
       <span class="lastused">
 % my $used = $token->LastUsedObj;
@@ -23,6 +32,17 @@ $tokens->LimitOwner(VALUE => $Owner);
         <&|/l&>never used</&>
 % }
       </span>
+
+      <div class="authtoken-form-container">
+        <& /Elements/AuthToken/ModifyForm, %ARGS, TokenObj => $token &>
+      </div>
+      <form method="GET" action="<%RT->Config->Get('WebPath')%><% $r->path_info %>#token-<% $token->Id %>">
+        <input type="hidden" name="ShowModifyForm" value="1">
+        <input type="hidden" name="id" value="<% $Owner %>">
+        <input type="hidden" name="Token" value="<% $token->Id %>">
+        <button type="submit" class="authtoken-modify">Edit</button>
+      </form>
+% }
     </li>
 % }
   </ul>
diff --git a/html/Elements/AuthToken/ModifyForm b/html/Elements/AuthToken/ModifyForm
new file mode 100644
index 0000000..f002df7
--- /dev/null
+++ b/html/Elements/AuthToken/ModifyForm
@@ -0,0 +1,53 @@
+<%ARGS>
+$Token => undef
+$TokenObj => undef
+$Error => ''
+</%ARGS>
+<%INIT>
+if (!$TokenObj) {
+    $TokenObj = RT::AuthToken->new($session{CurrentUser});
+    $TokenObj->Load($Token);
+}
+
+Abort("Unable to load authentication token") if !$TokenObj->Id;
+Abort("Permission Denied") if !$TokenObj->CurrentUserCanSee;
+</%INIT>
+<form class="authtoken-form" method="post" data-ajax-url="<% RT->Config->Get('WebPath') %>/Helpers/AuthToken/Modify" action="<% RT->Config->Get('WebPath') %><% $r->uri %>">
+% if ($Error) {
+<p class="error"><% $Error %></p>
+% }
+
+% if ($ARGS{id}) {
+<input type="hidden" name="id" value="<% $ARGS{id} %>">
+% }
+
+<input type="hidden" name="ShowModifyForm" value="1">
+<input type="hidden" name="Token" value="<% $TokenObj->id %>">
+<table>
+<tr>
+<td class="label"><&|/l&>Description</&>:<br><em><&|/l&>What's this token for?</&></em></td>
+<td class="value"><input type="text" name="Description" value="<% $ARGS{Description} // $TokenObj->Description %>" size="16" /></td>
+</tr>
+<tr>
+<td class="label"><&|/l&>Last Used</&>:</td>
+<td class="value">
+% my $used = $TokenObj->LastUsedObj;
+% if ($used->IsSet) {
+       <% $used->AgeAsString %>
+% } else {
+       <&|/l&>never</&>
+% }
+</td>
+</tr>
+<tr>
+<td class="label"><&|/l&>Created</&>:</td>
+<td class="value"><% $TokenObj->CreatedObj->AgeAsString %></td>
+</tr>
+</table>
+
+<div class="buttons">
+<input type="submit" name="Update" value="<&|/l&>Save</&>"></input>
+</div>
+
+<span class="loading"><img src="<%RT->Config->Get('WebPath')%>/static/images/loading.gif" alt="<%loc('Loading')%>" title="<%loc('Loading')%>" /></span>
+</form>
diff --git a/html/Elements/AuthToken/ModifyResults b/html/Elements/AuthToken/ModifyResults
new file mode 100644
index 0000000..4524250
--- /dev/null
+++ b/html/Elements/AuthToken/ModifyResults
@@ -0,0 +1,26 @@
+<%ARGS>
+$Token
+$Description => ''
+$Update => 0
+</%ARGS>
+<%INIT>
+my $TokenObj = RT::AuthToken->new($session{CurrentUser});
+$TokenObj->Load($Token);
+my ($error, $ok, $msg);
+
+if ($Update) {
+    if (!length($Description)) {
+        $error = loc("Description cannot be blank.");
+    }
+
+    if ($Description ne $TokenObj->Description) {
+        ($ok, $msg) = $TokenObj->SetDescription($Description);
+        $error = $msg if !$ok;
+    }
+}
+</%INIT>
+% if ($error || !$msg) {
+<& /Elements/AuthToken/ModifyForm, TokenObj => $TokenObj, Error => $error, Description => $Description, id => $ARGS{id} &>
+% } else {
+<% $msg %>
+% }
diff --git a/html/Helpers/AuthToken/Modify b/html/Helpers/AuthToken/Modify
new file mode 100644
index 0000000..25c0568
--- /dev/null
+++ b/html/Helpers/AuthToken/Modify
@@ -0,0 +1,2 @@
+<& /Elements/AuthToken/ModifyResults, %ARGS &>
+% $m->abort;
diff --git a/static/css/rt-authen-token.css b/static/css/rt-authen-token.css
index 72d489a..4c41632 100644
--- a/static/css/rt-authen-token.css
+++ b/static/css/rt-authen-token.css
@@ -59,3 +59,12 @@
 .authtoken-list.refreshing .loading {
     display: inline;
 }
+
+.authtoken-form input[name=Update] {
+    float: right;
+}
+
+.authtoken-form input[name=Revoke] {
+    float: left;
+}
+
diff --git a/static/js/rt-authen-token.js b/static/js/rt-authen-token.js
index a4b65a1..e8ac20e 100644
--- a/static/js/rt-authen-token.js
+++ b/static/js/rt-authen-token.js
@@ -11,6 +11,12 @@ jQuery(function() {
         showModal(jQuery('.authtoken-form-container').html());
     });
 
+    jQuery('.auth-tokens').on('click', '.authtoken-modify', function(e) {
+        e.preventDefault();
+        var container = jQuery(e.currentTarget).closest('li');
+        showModal(container.find('.authtoken-form-container').html());
+    });
+
     var refreshTokenList = function () {
         var list = jQuery('.authtoken-list');
         jQuery.post(

commit bf5feaab71907a8200ba3d9451a2ee5c11f25533
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 21:02:48 2017 +0000

    Web UI for revoking tokens

diff --git a/html/Elements/AuthToken/ModifyForm b/html/Elements/AuthToken/ModifyForm
index f002df7..0573f3a 100644
--- a/html/Elements/AuthToken/ModifyForm
+++ b/html/Elements/AuthToken/ModifyForm
@@ -47,6 +47,7 @@ Abort("Permission Denied") if !$TokenObj->CurrentUserCanSee;
 
 <div class="buttons">
 <input type="submit" name="Update" value="<&|/l&>Save</&>"></input>
+<input type="submit" name="Revoke" value="<&|/l&>Revoke</&>"></input>
 </div>
 
 <span class="loading"><img src="<%RT->Config->Get('WebPath')%>/static/images/loading.gif" alt="<%loc('Loading')%>" title="<%loc('Loading')%>" /></span>
diff --git a/html/Elements/AuthToken/ModifyResults b/html/Elements/AuthToken/ModifyResults
index 4524250..d561c26 100644
--- a/html/Elements/AuthToken/ModifyResults
+++ b/html/Elements/AuthToken/ModifyResults
@@ -2,6 +2,7 @@
 $Token
 $Description => ''
 $Update => 0
+$Revoke => 0
 </%ARGS>
 <%INIT>
 my $TokenObj = RT::AuthToken->new($session{CurrentUser});
@@ -18,6 +19,9 @@ if ($Update) {
         $error = $msg if !$ok;
     }
 }
+elsif ($Revoke) {
+    ($ok, $msg) = $TokenObj->Delete;
+}
 </%INIT>
 % if ($error || !$msg) {
 <& /Elements/AuthToken/ModifyForm, TokenObj => $TokenObj, Error => $error, Description => $Description, id => $ARGS{id} &>

commit 3354110637f991ce4c8f7a1173ffb5595c65841a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 6 22:12:25 2017 +0000

    Use constant_time_eq when available

diff --git a/lib/RT/AuthToken.pm b/lib/RT/AuthToken.pm
index b333248..9a31785 100644
--- a/lib/RT/AuthToken.pm
+++ b/lib/RT/AuthToken.pm
@@ -6,6 +6,7 @@ package RT::AuthToken;
 use base 'RT::Record';
 
 require RT::User;
+require RT::Util;
 use Digest::SHA 'sha512_hex';
 
 =head1 NAME
@@ -185,7 +186,14 @@ sub IsToken {
     # If it's a new-style (>= RT 4.0) password, it starts with a '!'
     my (undef, $method, @rest) = split /!/, $stored;
     if ($method eq "bcrypt") {
-        return 0 unless $self->_CryptToken_bcrypt($value, @rest) eq $stored;
+        if (RT::Util->can('constant_time_eq')) {
+            return 0 unless RT::Util::constant_time_eq(
+                $self->_CryptToken_bcrypt($value, @rest),
+                $stored,
+            );
+        } else {
+            return 0 unless $self->_CryptToken_bcrypt($value, @rest) eq $stored;
+        }
         # Upgrade to a larger number of rounds if necessary
         return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
     }

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list