# ====================================================================
# Gossamer Threads Module Library - http://gossamer-threads.com/
#
#   GT::Template::Editor
#   Author: Alex Krohn
#   CVS Info :                          
#   $Id: Editor.pm,v 2.19 2004/10/19 23:34:44 jagerman Exp $
#
# Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
# ====================================================================
#
# Description:
#   A module for editing templates via an HTML browser.
#

package GT::Template::Editor;
# ===============================================================
use strict;
use GT::Base;
use vars qw(@ISA $VERSION $DEBUG $ATTRIBS $ERRORS);
@ISA     = qw/GT::Base/;
$VERSION = sprintf "%d.%03d", q$Revision: 2.19 $ =~ /(\d+)\.(\d+)/;
$DEBUG   = 0;
$ATTRIBS = {
    cgi          => undef,
    root         => undef,
    backup       => undef,
    default_dir  => '',
    default_file => '',
    date_format  => '',
    class        => undef,
    skip_dir     => undef,
    skip_file    => undef,
    select_dir   => 'tpl_dir',
    demo         => undef
};
$ERRORS  = {
    CANTOVERWRITE => "Unable to overwrite file: %s (Permission Denied). Please set permissions properly and save again.",
    CANTCREATE    => "Unable to create new files in directory %s. Please set permissions properly and save again.",
    CANTMOVE      => "Unable to move file %s to %s: %s",
    CANTMOVE      => "Unable to copy file %s to %s: %s",
    FILECOPY      => "File::Copy is required in order to make backups.",
};

sub process {
# ------------------------------------------------------------------
# Loads the template editor.
#
    my $self = shift;

    my $sel_tpl_dir   = $self->{select_dir};
    my $selected_dir  = $self->{cgi}->param($sel_tpl_dir) || $self->{default_dir} || 'default';
    my $selected_file = $self->{cgi}->param('tpl_file') || '';
    my $tpl_text      = '';
    my $error_msg     = '';
    my $success_msg   = '';
    my ($local, $restore) = (0, 0);

# Check the template directory and file
    if ($selected_dir =~ m[[\\/\x00-\x1f]] or $selected_dir eq '..') {
        $error_msg = "Invalid template directory $selected_dir";
        $selected_dir = '';
        $selected_file = '';
    }
    if ($selected_file =~ m[[\\/\x00-\x1f]]) {
        $error_msg = "Invalid template $selected_file";
        $selected_dir = '';
        $selected_file = '';
    }

# Create the local directory if it doesn't exist.
    my $tpl_dir   = $self->{root} . '/' . $selected_dir;
    my $local_dir = $tpl_dir . "/local";
    if ($selected_dir and ! -d $local_dir) {
        mkdir($local_dir, 0777) or return $self->error('MKDIR', 'FATAL', $local_dir, "$!");
        chmod(0777, $local_dir);
    }
    my $dir = $local_dir;

    my $save = $self->{cgi}->param('tpl_name') || $self->{cgi}->param('tpl_file');
# Perform a save if requested.
    if ($self->{cgi}->param('saveas') and $save and !$self->{demo}) {
        $tpl_text = $self->{cgi}->param('tpl_text');
        if (-e "$dir/$save" and ! -w _) {
            $error_msg = sprintf($ERRORS->{CANTOVERWRITE}, $save);
        }
        elsif (! -e _ and ! -w $dir) {
            $error_msg = sprintf($ERRORS->{CANTCREATE}, $dir);
        }
        else {
            if ($self->{backup} and -e "$dir/$save") {
                $self->copy("$dir/$save", "$dir/$save.bak");
            }
            local *FILE;
            open (FILE, "> $dir/$save") or return $self->error(CANTOPEN => FATAL => "$dir/$save", "$!");
            $tpl_text =~ s/\r\n/\n/g;
            print FILE $tpl_text;
            close FILE;
            chmod 0666, "$dir/$save";
            $success_msg   = "File has been successfully saved.";
            $local         = 1;
            $restore       = 1 if -e "$self->{root}/$selected_dir/$save";
            $selected_file = $save;
            $tpl_text      = '';
        }
    }
# Delete a local template (thereby restoring the system template)
    elsif (my $restore = $self->{cgi}->param("restore") and !$self->{demo}) {
        if ($self->{backup}) {
            if ($self->move("$dir/$restore", "$dir/$restore.bak")) {
                $success_msg = "System template '$restore' restored";
            }
            else {
                $error_msg = "Unable to restore system template '$restore': Cannot move '$dir/$restore': $!";
            }
        }
        else {
            if (unlink "$dir/$restore") {
                $success_msg = "System template '$restore' restored";
            }
            else {
                $error_msg = "Unable to remove $dir/$restore: $!";
            }
        }
    }
# Delete a local template (This is like restore, but happens when there is no system template)
    elsif (my $delete = $self->{cgi}->param("delete") and !$self->{demo}) {
        if ($self->{backup}) {
            if ($self->move("$dir/$delete", "$dir/$delete.bak")) {
                $success_msg = "Template '$delete' deleted";
            }
            else {
                $error_msg = "Unable to delete template '$delete': Cannot move '$dir/$delete': $!";
            }
        }
        else {
            if (unlink "$dir/$delete") {
                $success_msg = "Template '$delete' deleted";
            }
            else {
                $error_msg = "Unable to remove $dir/$delete: $!";
            }
        }
    }

# Load any selected template file.
    if ($selected_file and ! $tpl_text) {
        if (-f "$dir/$selected_file") {
            local (*FILE, $/);
            open FILE, "$dir/$selected_file" or die "Unable to open file $dir/$selected_file: $!";
            $tpl_text = <FILE>;
            close FILE;
            $local = 1;
            $restore = 1 if -e "$self->{root}/$selected_dir/$selected_file";
        }
        elsif (-f "$self->{root}/$selected_dir/$selected_file") {
            local (*FILE, $/);
            open FILE, "$self->{root}/$selected_dir/$selected_file" or die "Unable to open file $self->{root}/$selected_dir/$selected_file: $!";
            $tpl_text = <FILE>;
            close FILE;
        }
        else {
            $selected_file = '';
        }
    }

# Load a README if it exists.
    my $readme;
    if (-e "$dir/README") {
        local (*FILE, $/);
        open FILE, "$dir/README" or die "unable to open readme: $dir/README ($!)";
        $readme = <FILE>;
        close FILE;
    }

# Set the textarea width and height.
    my $editor_rows = $self->{cgi}->param('cookie-editor_rows') || $self->{cgi}->cookie('editor_rows') || 15;
    my $editor_cols = $self->{cgi}->param('cookie-editor_cols') || $self->{cgi}->cookie('editor_cols') || 55;
    my $file_select = $self->template_file_select;
    my $dir_select  = $self->template_dir_select;
    $tpl_text = $self->{cgi}->html_escape($tpl_text);
    my $stats       = $selected_file ? $self->template_file_stats($selected_file) : {};

    if ($self->{demo} and ($self->{cgi}->param('saveas') or $self->{cgi}->param("delete") or $self->{cgi}->param("restore"))) {
        $error_msg = 'This feature has been disabled in the demo!';
    }
    return {
        tpl_name        => $selected_file,
        tpl_file        => $selected_file,
        local           => $local,
        restore         => $restore,
        tpl_text        => \$tpl_text,
        error_message   => $error_msg,
        success_message => $success_msg,
        tpl_dir         => $selected_dir,
        readme          => $readme,
        editor_rows     => $editor_rows,
        editor_cols     => $editor_cols,
        dir_select      => $dir_select,
        file_select     => $file_select,
        %$stats
    };
}

sub _skip_files {
    my ($skip, $file) = @_;
    return 1 if $skip->{$file}
                or substr($file, 0, 1) eq '.' # skip dotfiles
                or substr($file, -4) eq '.bak'; # skip .bak files
    foreach my $f (keys %$skip) {
        my $match = quotemeta $f;
        $match =~ s/\\\*/.*/g;
        $match =~ s/\\\?/./g;
        return 1 if $file =~ /^$match$/;
    }
    return;
}

sub template_file_select {
# ------------------------------------------------------------------
# Returns a select list of templates in a given dir.
#
    my $self = shift;
    my $path = $self->{root};
    my %files;
    my $sel_tpl_dir   = $self->{select_dir};
    my $selected_dir  = $self->{cgi}->param($sel_tpl_dir) || $self->{default_dir} || 'default';
    my $selected_file = $self->{cgi}->param('tpl_file') || $self->{default_file} || 'default';
    $selected_file    = $self->{cgi}->param('tpl_name') if $self->{cgi}->param('saveas');
    my %skip;
    if ($self->{skip_file}) {
        for (@{$self->{skip_file}}) {
            $skip{$_}++;
        }
    }
    else {
        $skip{README} = $skip{'language.txt'} = $skip{'globals.txt'} = 1;
    }

# Check the template directory
    return if $selected_dir =~ m[[\\/\x00-\x1f]] or $selected_dir eq '..';

    my $system_dir = $path . "/" . $selected_dir;
    my $local_dir  = $path . "/" . $selected_dir . '/local';
    foreach my $dir ($system_dir, $local_dir) {
        opendir (TPL, $dir) or next;
        while (defined(my $file = readdir TPL)) {
            next unless -f "$dir/$file" and -r _;
            next if _skip_files(\%skip, $file);

            $files{$file} = 1;
        }
        closedir TPL;
    }
    my $f_select_list = '<select name="tpl_file"';
    $f_select_list .= qq' class="$self->{class}"' if $self->{class};
    $f_select_list .= ">\n";

    foreach (sort keys %files) {
        my $system = -e $path . '/' . $selected_dir . '/' . $_;
        my $local = -e $path . '/' . $selected_dir . '/local/' . $_;
        my $changed = $system && $local ? ' *' : $local ? ' +' : '';
        $f_select_list .= qq'  <option value="$_"';
        $f_select_list .= ' selected' if $_ eq $selected_file;
        $f_select_list .= ">$_$changed</option>\n";
    }
    $f_select_list .= "</select>";

    return $f_select_list;
}

sub template_dir_select {
# ------------------------------------------------------------------
# Returns a select list of template directories.
#
    my $self = shift;
    my ($dir, $file, @dirs);
    my $name         = $self->{select_dir};
    my $selected_dir = $self->{cgi}->param($name) || $self->{default_dir} || 'default';

    $dir = $self->{root};

    my %skip = ('..' => 1, '.' => 1);
    if ($self->{skip_dir}) {
        for (@{$self->{skip_dir}}) { $skip{$_}++ }
    }
    else {
        $skip{admin} = $skip{help} = $skip{CVS} = 1;
    }

    opendir (TPL, $dir) or die "unable to open directory: '$dir' ($!)";
    while (defined($file = readdir TPL)) {
        next if $skip{$file};
        next unless (-d "$dir/$file");
        push @dirs, $file;
    }
    closedir TPL;

    my $d_select_list = qq'<select name="$name"';
    $d_select_list .= qq' class="$self->{class}"' if $self->{class};
    $d_select_list .= ">\n";
    foreach (sort @dirs) {
        $d_select_list .= qq'  <option value="$_"';
        $d_select_list .= ' selected' if $_ eq $selected_dir;
        $d_select_list .= ">$_</option>\n";
    }
    $d_select_list .= "</select>";
    return $d_select_list;
}

sub template_file_stats {
# ------------------------------------------------------------------
# Returns information about a file. Takes the following arguments:
#   - filename
#   - template set
# The following tags are returned:
#   - file_path - the full path to the file, relative to the admin root directory
#   - file_size - the size of the file in bytes
#   - file_local - 1 or 0 - true if it is a local file
#   - file_restore - 1 or 0 - true if it is a local file and a non-local file of the same name exists (The non-local can be restored)
#   - file_mod_time - the date the file was last modified
#
    require GT::Date;
    my ($self, $file) = @_;
    my $sel_tpl_dir   = $self->{select_dir};
    my $tpl_dir       = $self->{cgi}->param($sel_tpl_dir) || $self->{default_dir} || 'default';
    my $return = { file_local => 1, file_restore => 1 };
    my $dir = "$self->{root}/$tpl_dir";
    if (-f "$dir/local/$file" and -r _) {
        $return->{file_path} = "templates/$tpl_dir/local/$file";
        $return->{file_size} = -s _;
        $return->{file_local} = 1;
        my $mod_time = (stat _)[9];
        $return->{file_restore} = (-f "$dir/$file" and -r _) ? 1 : 0;
        if ($self->{date_format}) {
            require GT::Date;
            $return->{file_mod_time} = GT::Date::date_get($mod_time, $self->{date_format});
        }
        else {
            $return->{file_mod_time} = localtime($mod_time);
        }
    }
    else {
        $return->{file_path} = "templates/$tpl_dir/$file";
        $return->{file_size} = -s "$dir/$file";
        $return->{file_local} = 0;
        $return->{file_restore} = 0;
        my $mod_time = (stat _)[9];
        if ($self->{date_format}) {
            require GT::Date;
            $return->{file_mod_time} = GT::Date::date_get($mod_time, $self->{date_format});
        }
        else {
            $return->{file_mod_time} = localtime($mod_time);
        }
    }
    return $return;
}

sub move {
# -------------------------------------------------------------------
# Uses File::Copy to move a file.
#
    my $self = shift;
    my ($from, $to) = @_;
    eval { require File::Copy; };
    if ($@) {
        return $self->error('FILECOPY', $@);
    }
    File::Copy::mv($from, $to) or return $self->error('CANTMOVE', $from, $to, "$!");
}

sub copy {
# -------------------------------------------------------------------
# Uses File::Copy to move a file.
#
    my $self = shift;
    my ($from, $to) = @_;
    eval { require File::Copy; };
    if ($@) {
        return $self->error('FILECOPY', $@);
    }
    File::Copy::cp($from, $to) or return $self->error('CANTCOPY', $from, $to, "$!");
}

__END__

=head1 NAME

GT::Template::Editor - This module provides an easy way to edit templates.

=head1 SYNOPSIS

Should be called like:

    require GT::Template::Editor;
    my $editor = new GT::Template::Editor (
                    root        => $CFG->{admin_root_path} . '/templates',
                    default_dir => $CFG->{build_default_tpl},
                    backup      => 1,
                    cgi         => $IN
                );
    return $editor->process;

and it returns a hsah ref of variables used for displaying a template editor page.

=head1 COPYRIGHT

Copyright (c) 2004 Gossamer Threads Inc.  All Rights Reserved.
http://www.gossamer-threads.com/

=head1 VERSION

Revision: $Id: Editor.pm,v 2.19 2004/10/19 23:34:44 jagerman Exp $

=cut

