package Daizu;
use warnings;
use strict;
use XML::LibXML;
use DBI;
use SVN::Ra;
use Path::Class qw( dir );
use Carp qw( croak );
use Carp::Assert qw( assert DEBUG );
use Daizu::Revision;
use Daizu::Wc;
use Daizu::Util qw(
trim trim_with_empty_null
validate_number validate_uri validate_mime_type
validate_date db_datetime
db_row_exists db_row_id db_select db_insert db_update db_delete
wc_file_data
guid_first_last_times
load_class
xml_attr xml_croak
daizu_data_dir
);
=head1 NAME
Daizu - class for accessing Daizu CMS from Perl
=head1 INTRODUCTION
Daizu CMS is an experimental content management system. It uses content
stored in a Subversion repository, and keeps track of it in a PostgreSQL
database. It is an attempt to solve some of the underlying problems of
content management once and for all. As such the development so far has
focused on the 'back end' parts of the system, and it doesn't really have
a user interface to speak of. It's certainly not ready for less technical
users yet. More information is available on the Daizu website:
L
=head1 DESCRIPTION
Most access to Daizu functionality requires a Daizu object. It provides
a database handle for access to the 'live' content data, and a L
object for access to the Subversion repository.
Some other classes are documented as requiring a C<$cms> value as the
first argument to their constructors or methods. This should always be
a Daizu object.
=head2 CONSTANTS
=over
=item $Daizu::VERSION
The version number of Daizu CMS (as a whole, not just this module).
=item $Daizu::DEFAULT_CONFIG_FILENAME
The full path and filename of the config file which will be read by
default, if none is specified in the constructor call or the environment.
Value: I
=item $Daizu::CONFIG_NS
The URI used as an XML namespace for the elements in the config file.
Value: L
=item $Daizu::HTML_EXTENSION_NS
The URI used as an XML namespace for special elements in XHTML content.
Value: L
=item $Daizu::HIDING_FILENAMES
A list of file and directory names which prevent any publication of
files with one of the names, or anything inside a directory so named.
Separated by '|' so that the whole string can be included in Perl
and PostgreSQL regular expressions.
Value: C<_template|_hide>
=cut
our $VERSION = '0.3';
our $DEFAULT_CONFIG_FILENAME = '/etc/daizu/config.xml';
our $CONFIG_NS = 'http://www.daizucms.org/ns/config/';
our $HTML_EXTENSION_NS = 'http://www.daizucms.org/ns/html-extension/';
our $HIDING_FILENAMES = '_template|_hide|_lib';
=item %OVERRIDABLE_PROPERTY
A hash describing which pieces of metadata can be overridden by article
loader plugins. The keys are the names of Subversion properties, and
the values are the names of columns in the C table.
=cut
our %OVERRIDABLE_PROPERTY = (
'dc:title' => 'title',
'dc:description' => 'description',
'daizu:short-title' => 'short_title',
);
=back
=head2 METHODS
=over
=item Daizu-Enew($config_filename)
Return a Daizu object based on the information in the given configuration
file. If C<$config_filename> is not supplied, it will fall back on any
file specified by the C environment variable, and then
by the default config file (see C<$DEFAULT_CONFIG_FILENAME> above).
The value returned will be called C<$cms> in the documentation.
For information about the format of the configuration file, see
the documentation on the website:
L
=cut
# This ensures that @INC is only fiddled with once for each Daizu installation.
# The keys are the URIs of content repositories. If an entry exists for a
# particular repository, then its _lib directory has already been added.
my %added_lib_path;
sub new
{
my ($class, $filename) = @_;
if (!defined $filename) {
if (defined $ENV{DAIZU_CONFIG}) {
$filename = $ENV{DAIZU_CONFIG};
}
elsif (-r $DEFAULT_CONFIG_FILENAME) {
$filename = $DEFAULT_CONFIG_FILENAME;
}
else {
croak "cannot find Daizu configuration file" .
" (set DAIZU_CONFIG environment variable)";
}
}
croak "Bad config file '$filename', not a normal file\n"
unless -f $filename;
my $self = bless { config_filename => $filename }, $class;
my $parser = XML::LibXML->new;
my $doc = $parser->parse_file($filename);
my $root = $doc->documentElement;
xml_croak($filename, $root, "root element must be ")
unless $root->localname eq 'config';
xml_croak($filename, $root, "root element in wrong namespace")
unless defined $root->namespaceURI && $root->namespaceURI eq $CONFIG_NS;
# Open database connection.
{
my $elem = _singleton_conf_elem($filename, $root, 'database');
my $dsn = xml_attr($filename, $elem, 'dsn');
my $user = $elem->getAttribute('user');
die "$filename: should have 'user' attribute, not 'username'"
if !defined $user && $elem->hasAttribute('username');
my $password = $elem->getAttribute('password');
$self->{db} = DBI->connect($dsn, $user, $password, {
AutoCommit => 1,
RaiseError => 1,
PrintError => 0,
});
}
# Open Subversion remote-access connection.
my $svn_url;
{
my $elem = _singleton_conf_elem($filename, $root, 'repository');
$svn_url = xml_attr($filename, $elem, 'url');
my $svn_username = xml_attr($filename, $elem, 'username', '');
my $svn_password = xml_attr($filename, $elem, 'password', '');
my $auth_callback = sub {
my ($creds, $realm, $default_username, $may_save, $pool) = @_;
$creds->username($svn_username);
$creds->password($svn_password);
# There's no real reason to cache this stuff since we can always
# get it from the config files, so we don't cache to avoid
# confusion, and in case we're running as a special user with
# a home directory we can't write to.
$creds->may_save(0);
};
$self->{ra} = SVN::Ra->new(
url => $svn_url,
($svn_username eq '' && $svn_password eq '' ? () : (auth => [
SVN::Client::get_simple_prompt_provider($auth_callback, 0),
])),
);
}
# Get live working copy ID.
{
my $elem = _singleton_conf_elem($filename, $root, 'live-working-copy');
my $wc_id = xml_attr($filename, $elem, 'id');
$self->{live_wc_id} = validate_number($wc_id);
xml_croak($filename, $elem, "bad WC ID in ")
unless defined $self->{live_wc_id};
}
# Path to directory containing the default templates distributed with
# Daizu, and possibly also to a directory where templates should be
# loaded during testing instead of from the database.
{
$self->{template_default_path} = daizu_data_dir('template');
my ($elem) = $root->getChildrenByTagNameNS($CONFIG_NS, 'template-test');
$self->{template_test_path} = xml_attr($filename, $elem, 'path')
if defined $elem;
}
# Add to @INC the '_lib' directory from the content repository, either
# by loading files from the live working copy, or from the 'template-test'
# path.
unless (exists $added_lib_path{$svn_url}) {
if (defined $self->{template_test_path}) {
push @INC, dir($self->{template_test_path})->subdir('_lib')
->stringify;
}
else {
push @INC, sub {
my (undef, $filename) = @_;
my $file_id = db_row_id($self->{db}, 'wc_file',
wc_id => $self->{live_wc_id},
path => "_lib/$filename",
);
return undef unless defined $file_id;
my $data = wc_file_data($self->{db}, $file_id);
open my $fh, '<', $data
or die "error opening memory file for '_lib/$filename': $!";
return $fh;
};
}
$added_lib_path{$svn_url} = undef;
}
# How output should be published.
for my $elem ($root->getChildrenByTagNameNS($CONFIG_NS, 'output')) {
my $url = trim(xml_attr($filename, $elem, 'url'));
my $path = trim(xml_attr($filename, $elem, 'path'));
my $url_ob = validate_uri($url);
xml_croak($filename, $elem, "