discourse-legacysite-perl/site/glist/lib/GT/Template/Editor.pm
2024-06-17 21:49:12 +10:00

418 lines
14 KiB
Perl

# ====================================================================
# 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