File Coverage

blib/lib/Daizu.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Daizu;
2 12     12   1164616 use warnings;
  12         33  
  12         1296  
3 12     12   70 use strict;
  12         26  
  12         470  
4              
5 12     12   33427 use XML::LibXML;
  0            
  0            
6             use DBI;
7             use SVN::Ra;
8             use Path::Class qw( dir );
9             use Carp qw( croak );
10             use Carp::Assert qw( assert DEBUG );
11             use Daizu::Revision;
12             use Daizu::Wc;
13             use Daizu::Util qw(
14             trim trim_with_empty_null
15             validate_number validate_uri validate_mime_type
16             validate_date db_datetime
17             db_row_exists db_row_id db_select db_insert db_update db_delete
18             wc_file_data
19             guid_first_last_times
20             load_class
21             xml_attr xml_croak
22             daizu_data_dir
23             );
24              
25             =head1 NAME
26              
27             Daizu - class for accessing Daizu CMS from Perl
28              
29             =head1 INTRODUCTION
30              
31             Daizu CMS is an experimental content management system. It uses content
32             stored in a Subversion repository, and keeps track of it in a PostgreSQL
33             database. It is an attempt to solve some of the underlying problems of
34             content management once and for all. As such the development so far has
35             focused on the 'back end' parts of the system, and it doesn't really have
36             a user interface to speak of. It's certainly not ready for less technical
37             users yet. More information is available on the Daizu website:
38              
39             L
40              
41             =head1 DESCRIPTION
42              
43             Most access to Daizu functionality requires a Daizu object. It provides
44             a database handle for access to the 'live' content data, and a L
45             object for access to the Subversion repository.
46              
47             Some other classes are documented as requiring a C<$cms> value as the
48             first argument to their constructors or methods. This should always be
49             a Daizu object.
50              
51             =head2 CONSTANTS
52              
53             =over
54              
55             =item $Daizu::VERSION
56              
57             The version number of Daizu CMS (as a whole, not just this module).
58              
59             =item $Daizu::DEFAULT_CONFIG_FILENAME
60              
61             The full path and filename of the config file which will be read by
62             default, if none is specified in the constructor call or the environment.
63              
64             Value: I
65              
66             =item $Daizu::CONFIG_NS
67              
68             The URI used as an XML namespace for the elements in the config file.
69              
70             Value: L
71              
72             =item $Daizu::HTML_EXTENSION_NS
73              
74             The URI used as an XML namespace for special elements in XHTML content.
75              
76             Value: L
77              
78             =item $Daizu::HIDING_FILENAMES
79              
80             A list of file and directory names which prevent any publication of
81             files with one of the names, or anything inside a directory so named.
82             Separated by '|' so that the whole string can be included in Perl
83             and PostgreSQL regular expressions.
84              
85             Value: C<_template|_hide>
86              
87             =cut
88              
89             our $VERSION = '0.3';
90              
91             our $DEFAULT_CONFIG_FILENAME = '/etc/daizu/config.xml';
92             our $CONFIG_NS = 'http://www.daizucms.org/ns/config/';
93             our $HTML_EXTENSION_NS = 'http://www.daizucms.org/ns/html-extension/';
94             our $HIDING_FILENAMES = '_template|_hide|_lib';
95              
96             =item %OVERRIDABLE_PROPERTY
97              
98             A hash describing which pieces of metadata can be overridden by article
99             loader plugins. The keys are the names of Subversion properties, and
100             the values are the names of columns in the C table.
101              
102             =cut
103              
104             our %OVERRIDABLE_PROPERTY = (
105             'dc:title' => 'title',
106             'dc:description' => 'description',
107             'daizu:short-title' => 'short_title',
108             );
109              
110             =back
111              
112             =head2 METHODS
113              
114             =over
115              
116             =item Daizu-Enew($config_filename)
117              
118             Return a Daizu object based on the information in the given configuration
119             file. If C<$config_filename> is not supplied, it will fall back on any
120             file specified by the C environment variable, and then
121             by the default config file (see C<$DEFAULT_CONFIG_FILENAME> above).
122              
123             The value returned will be called C<$cms> in the documentation.
124              
125             For information about the format of the configuration file, see
126             the documentation on the website:
127             L
128              
129             =cut
130              
131             # This ensures that @INC is only fiddled with once for each Daizu installation.
132             # The keys are the URIs of content repositories. If an entry exists for a
133             # particular repository, then its _lib directory has already been added.
134             my %added_lib_path;
135              
136             sub new
137             {
138             my ($class, $filename) = @_;
139              
140             if (!defined $filename) {
141             if (defined $ENV{DAIZU_CONFIG}) {
142             $filename = $ENV{DAIZU_CONFIG};
143             }
144             elsif (-r $DEFAULT_CONFIG_FILENAME) {
145             $filename = $DEFAULT_CONFIG_FILENAME;
146             }
147             else {
148             croak "cannot find Daizu configuration file" .
149             " (set DAIZU_CONFIG environment variable)";
150             }
151             }
152              
153             croak "Bad config file '$filename', not a normal file\n"
154             unless -f $filename;
155              
156             my $self = bless { config_filename => $filename }, $class;
157              
158             my $parser = XML::LibXML->new;
159             my $doc = $parser->parse_file($filename);
160             my $root = $doc->documentElement;
161             xml_croak($filename, $root, "root element must be ")
162             unless $root->localname eq 'config';
163             xml_croak($filename, $root, "root element in wrong namespace")
164             unless defined $root->namespaceURI && $root->namespaceURI eq $CONFIG_NS;
165              
166             # Open database connection.
167             {
168             my $elem = _singleton_conf_elem($filename, $root, 'database');
169             my $dsn = xml_attr($filename, $elem, 'dsn');
170             my $user = $elem->getAttribute('user');
171             die "$filename: should have 'user' attribute, not 'username'"
172             if !defined $user && $elem->hasAttribute('username');
173             my $password = $elem->getAttribute('password');
174             $self->{db} = DBI->connect($dsn, $user, $password, {
175             AutoCommit => 1,
176             RaiseError => 1,
177             PrintError => 0,
178             });
179             }
180              
181             # Open Subversion remote-access connection.
182             my $svn_url;
183             {
184             my $elem = _singleton_conf_elem($filename, $root, 'repository');
185             $svn_url = xml_attr($filename, $elem, 'url');
186             my $svn_username = xml_attr($filename, $elem, 'username', '');
187             my $svn_password = xml_attr($filename, $elem, 'password', '');
188              
189             my $auth_callback = sub {
190             my ($creds, $realm, $default_username, $may_save, $pool) = @_;
191              
192             $creds->username($svn_username);
193             $creds->password($svn_password);
194              
195             # There's no real reason to cache this stuff since we can always
196             # get it from the config files, so we don't cache to avoid
197             # confusion, and in case we're running as a special user with
198             # a home directory we can't write to.
199             $creds->may_save(0);
200             };
201              
202             $self->{ra} = SVN::Ra->new(
203             url => $svn_url,
204             ($svn_username eq '' && $svn_password eq '' ? () : (auth => [
205             SVN::Client::get_simple_prompt_provider($auth_callback, 0),
206             ])),
207             );
208             }
209              
210             # Get live working copy ID.
211             {
212             my $elem = _singleton_conf_elem($filename, $root, 'live-working-copy');
213             my $wc_id = xml_attr($filename, $elem, 'id');
214             $self->{live_wc_id} = validate_number($wc_id);
215             xml_croak($filename, $elem, "bad WC ID in ")
216             unless defined $self->{live_wc_id};
217             }
218              
219             # Path to directory containing the default templates distributed with
220             # Daizu, and possibly also to a directory where templates should be
221             # loaded during testing instead of from the database.
222             {
223             $self->{template_default_path} = daizu_data_dir('template');
224             my ($elem) = $root->getChildrenByTagNameNS($CONFIG_NS, 'template-test');
225             $self->{template_test_path} = xml_attr($filename, $elem, 'path')
226             if defined $elem;
227             }
228              
229             # Add to @INC the '_lib' directory from the content repository, either
230             # by loading files from the live working copy, or from the 'template-test'
231             # path.
232             unless (exists $added_lib_path{$svn_url}) {
233             if (defined $self->{template_test_path}) {
234             push @INC, dir($self->{template_test_path})->subdir('_lib')
235             ->stringify;
236             }
237             else {
238             push @INC, sub {
239             my (undef, $filename) = @_;
240             my $file_id = db_row_id($self->{db}, 'wc_file',
241             wc_id => $self->{live_wc_id},
242             path => "_lib/$filename",
243             );
244             return undef unless defined $file_id;
245             my $data = wc_file_data($self->{db}, $file_id);
246             open my $fh, '<', $data
247             or die "error opening memory file for '_lib/$filename': $!";
248             return $fh;
249             };
250             }
251              
252             $added_lib_path{$svn_url} = undef;
253             }
254              
255             # How output should be published.
256             for my $elem ($root->getChildrenByTagNameNS($CONFIG_NS, 'output')) {
257             my $url = trim(xml_attr($filename, $elem, 'url'));
258             my $path = trim(xml_attr($filename, $elem, 'path'));
259             my $url_ob = validate_uri($url);
260             xml_croak($filename, $elem, " has invalid URL '$url'")
261             unless defined $url_ob;
262             xml_croak($filename, $elem, " has non-HTTP URL '$url'")
263             unless defined $url_ob->scheme && $url_ob->scheme =~ /^https?/i;
264             $url = $url_ob->canonical;
265             xml_croak($filename, $elem, "more than one element for '$url'")
266             if exists $self->{output}{$url};
267              
268             my $redirect_map = trim(xml_attr($filename, $elem, 'redirect-map', ''));
269             my $gone_map = trim(xml_attr($filename, $elem, 'gone-map', ''));
270             for ($redirect_map, $gone_map) {
271             $_ = undef if $_ eq '';
272             next unless defined;
273              
274             # Check for duplicate filenames.
275             while (my ($other_url, $config) = each %{$self->{output}}) {
276             for my $map (qw( redirect gone )) {
277             xml_croak($filename, $elem, "filename '$_' duplicates" .
278             " '$map-map' for '$other_url' config")
279             if defined $config->{"${map}_map"} &&
280             $config->{"${map}_map"} eq $_;
281             }
282             }
283             }
284              
285             my $index_filename = trim(xml_attr($filename, $elem, 'index-filename',
286             'index.html'));
287              
288             $self->{output}{$url} = {
289             url => $url_ob,
290             path => $path,
291             redirect_map => $redirect_map,
292             gone_map => $gone_map,
293             index_filename => $index_filename,
294             };
295             }
296              
297             # Initialize hooks for plugins.
298             $self->{property_loaders}{'*'} = [ [ $self => '_std_property_loader' ] ];
299             $self->{html_dom_filters} = {};
300             $self->{article_loaders} = {};
301              
302             # Read global configuration for things which can be overridden for
303             # specific paths.
304             $self->_read_config_for_path($filename, $root, '');
305             xml_croak($filename, $root, "no default element")
306             unless defined $self->{default_entity};
307              
308             # Read path-specific configuration in each inner element.
309             for my $elem ($root->getChildrenByTagNameNS($CONFIG_NS, 'config')) {
310             xml_croak($filename, $elem, "inner elements must have path")
311             unless $elem->hasAttribute('path');
312             my $path = $elem->getAttribute('path');
313             xml_croak($filename, $elem, "inner element's path is empty")
314             if $path eq '';
315             $self->_read_config_for_path($filename, $elem, $path);
316             }
317              
318             return $self;
319             }
320              
321             sub _read_config_for_path
322             {
323             my ($self, $filename, $config, $path) = @_;
324             xml_croak($filename, $config, " element has bad path '$path'")
325             if $path =~ /^\// || $path =~ /\/$/;
326              
327             # Load information for minting GUID URLs.
328             for my $elem ($config->getChildrenByTagNameNS($CONFIG_NS, 'guid-entity')) {
329             my $entity = trim(xml_attr($filename, $elem, 'entity'));
330             xml_croak($filename, $elem, " has empty entity")
331             if $entity eq '';
332              
333             if ($path eq '') {
334             xml_croak($filename, $elem,
335             "more than one default (pathless) element")
336             if defined $self->{default_entity};
337             $self->{default_entity} = $entity;
338             }
339             else {
340             xml_croak($filename, $elem,
341             "more than one for path '$path'")
342             if exists $self->{path_entity}{$path};
343             $self->{path_entity}{$path} = $entity;
344             }
345             }
346              
347             # Load and register plugins.
348             for my $elem ($config->getChildrenByTagNameNS($CONFIG_NS, 'plugin')) {
349             my $class = trim(xml_attr($filename, $elem, 'class'));
350             load_class($class);
351             $class->register($self, $config, $elem, $path);
352             }
353              
354             # Configuration for generator classes
355             for my $elem ($config->getChildrenByTagNameNS($CONFIG_NS, 'generator')) {
356             my $class = trim(xml_attr($filename, $elem, 'class'));
357             xml_croak($filename, $elem,
358             "only one generator config allowed for '$class' at '$path'")
359             if exists $self->{generator_config}{$class}{$path};
360             $self->{generator_config}{$class}{$path} = $elem;
361             }
362             }
363              
364             # Return a named element which must be a child of the specified $root element,
365             # and check that there is exactly one of them.
366             sub _singleton_conf_elem
367             {
368             my ($filename, $root, $name) = @_;
369             my ($elem, $extra) = $root->getChildrenByTagNameNS($CONFIG_NS, $name);
370             xml_croak($filename, $root, "missing <$name> element")
371             unless defined $elem;
372             xml_croak($filename, $extra, "only one <$name> element is allowed")
373             if defined $extra;
374             return $elem;
375             }
376              
377             =item $cms-Era
378              
379             Return the Subversion remote access (L) object for accessing the
380             repository.
381              
382             =cut
383              
384             sub ra { $_[0]->{ra} }
385              
386             =item $cms-Edb
387              
388             Return the L database handle for accessing the Daizu database.
389              
390             =cut
391              
392             sub db { $_[0]->{db} }
393              
394             =item $cms-Econfig_filename
395              
396             Returns a string containing the filename from which the configuration
397             was loaded. The filename may be a full (absolute) path, or may be
398             relative to the current directory at the time the Daizu object was
399             created.
400              
401             =cut
402              
403             sub config_filename { $_[0]->{config_filename} }
404              
405             =item $cms-Elive_wc
406              
407             Return a L object representing the live working copy.
408              
409             =cut
410              
411             sub live_wc
412             {
413             my ($self) = @_;
414             return Daizu::Wc->new($self);
415             }
416              
417             =item $cms-Eload_revision($update_to_rev)
418              
419             Load information about revisions and file paths for any new revisions,
420             upto C<$update_to_rev>, from the repository into the database. If no
421             revision number is supplied, updates to the latest revision.
422              
423             This is called automatically before any working copy updates, to ensure
424             that the database knows about revisions before any working copies are
425             updated to them. It is idempotent.
426              
427             This is a simple wrapper round the code in L.
428              
429             =cut
430              
431             sub load_revision
432             {
433             my ($self, $update_to_rev) = @_;
434             return Daizu::Revision::load_revision($self, $update_to_rev);
435             }
436              
437             =item $cms-Eadd_property_loader($pattern, $object, $method)
438              
439             Plugins can use this to register themselves as a 'property loader',
440             which will be called when a property whose name matches C<$pattern>
441             is updated in a working copy.
442              
443             Currently it isn't possible to localize property loader plugins to
444             have different configuration for different paths in the repository
445             using the normal path configuration system.
446              
447             The pattern can be either the exact property name, a wildcard match on
448             some prefix of the name ending in a colon, such as C, or just
449             a C<*> which will match all property names. There isn't any generic
450             wildcard or regular expression matching capability.
451              
452             C<$object> should be an object (probably of the plugin's class) on which
453             C<$method> can be called. Since it is called as a method, the first
454             value passed in will be C<$object>, followed by these:
455              
456             =over
457              
458             =item $cms
459              
460             A C object.
461              
462             =item $id
463              
464             The ID number of the file in the C database table for which the
465             new property values apply.
466              
467             =item $props
468              
469             A reference to a hash of the new property values.
470             Only properties which have been
471             changed during a working copy update will have entries, so the file
472             may have other properties which haven't been changed.
473              
474             Properties which have been deleted during the update will have an
475             entry in this hash with a value of C.
476              
477             =back
478              
479             An example of a property loader method is C<_std_property_loader> in
480             this module. It is always registered automatically.
481              
482             =cut
483              
484             sub add_property_loader
485             {
486             my ($self, $pattern, $object, $method) = @_;
487             push @{$self->{property_loaders}{$pattern}}, [ $object => $method ];
488             }
489              
490             =item $cms-Eadd_article_loader($mime_type, $path, $object, $method)
491              
492             Plugins can use this to register a method which will be called whenever
493             an article of type C<$mime_type> needs to be loaded. The MIME type can be
494             fully specified, or be something like C (to match any image format),
495             or just be C<*> to match any type. These aren't generic glob or regex
496             patterns, so only those three levels of specificity are allowed. The
497             most specific plugin available will be tried first. Plugins of the same
498             specificity will be tried in the order they are registered. The plugin
499             methods can return false if they can't handle a particular file for
500             some reason, in which case Daizu will continue to look for another suitable
501             plugin.
502              
503             The plugin registered will only be called on for files with paths which
504             are the same as, or are under the directory specified by, C<$path>.
505             Plugins should usually just pass the C<$path> value from their C
506             method through to this method as-is.
507              
508             C<$method> (a method name) will be called on C<$object>, and will be
509             passed C<$cms> and a
510             L object representing the input file. The method should
511             return a hash of values describing the article. Alternatively it can
512             return false to indicate that it can't handle the file.
513              
514             The hash returned can contain the following values:
515              
516             =over
517              
518             =item content
519              
520             Required. All the other values are optional.
521              
522             This should be an XHTML DOM of the article's content, as it will be published.
523             It should be an L object, with a root element called
524             C in the XHTML namespace. It can contain extension elements to be
525             processed by article filter plugins. It can contain XInclude elements,
526             which will be processed by the
527             L.
528             Entity references should not be present.
529              
530             =item title
531              
532             The title to use for the article. If this is present and not undef then
533             it will override the value of the C property.
534              
535             =item short_title
536              
537             The 'short title' to use for the article. If this is present and not
538             undef then it will override the value of the C property.
539              
540             =item description
541              
542             The description to use for the article. If this is present and not undef then
543             it will override the value of the C property.
544              
545             =item pages_url
546              
547             The URL to use for the first page of the article, and which will also be
548             used to generate URLs for subsequent pages (if any). This can be absolute,
549             or relative to the file's base URL.
550              
551             =item extra_urls
552              
553             A reference to an array of URL info hashes describing extra URLs generated
554             by the file in addition to the actual pages of the article. These are
555             stored in the C table.
556              
557             =item extra_templates
558              
559             A reference to an array of filenames of extra templates to be included in
560             the article's 'extras' column. These are stored in the
561             C table.
562              
563             =back
564              
565             See L or L for
566             examples of registering and writing article loader plugins.
567              
568             =cut
569              
570             sub add_article_loader
571             {
572             my ($self, $mime_type, $path, $object, $method) = @_;
573             push @{$self->{article_loaders}{$mime_type}{$path}}, [ $object => $method ];
574             }
575              
576             =item $cms-Eadd_html_dom_filter($path, $object, $method)
577              
578             Plugins can use this to register a method which will be called whenever
579             an XHTML file is being published. C<$method> (a method name) will be
580             called on C<$object>, and will be passed C<$cms>, a L object
581             for the file being filtered, and an XML DOM object
582             of the source, as a L object. The plugin method
583             should return a reference to a hash containing a C value which
584             is the filtered content, either a completely new copy of the DOM
585             or the same value it was passed (which it might have modified in place).
586              
587             The returned hash can also contain an C array, in the same
588             way as an article loader, if the filter adds additional URLs for the file.
589              
590             The plugin registered will only be called on for files with paths which
591             are the same as, or are under the directory specified by, C<$path>.
592             Plugins should usually just pass the C<$path> value from their C
593             method through to this method as-is.
594              
595             See L for an example of registering and
596             implementing a DOM filter method.
597              
598             =cut
599              
600             sub add_html_dom_filter
601             {
602             my ($self, $path, $object, $method) = @_;
603             my $filter_name = ref($object) . "->$method"; # just for a hash key
604             croak "HTML DOM filter already defined for '$filter_name' at '$path'"
605             if exists $self->{html_dom_filters}{$filter_name}{$path};
606             $self->{html_dom_filters}{$filter_name}{$path} = [ $object => $method ];
607             }
608              
609             sub _std_property_loader
610             {
611             my ($self, undef, $id, $props) = @_;
612             my $db = $self->{db};
613             my %update;
614              
615             $update{content_type} = validate_mime_type($props->{'svn:mime-type'})
616             if exists $props->{'svn:mime-type'};
617              
618             if (exists $props->{'dcterms:issued'}) {
619             my $time = validate_date($props->{'dcterms:issued'});
620             warn "file $id has invalid 'dcterms:issued' datetime, ignoring\n"
621             if !defined $time && defined $props->{'dcterms:issued'};
622             # If the custom publication datetime is removed, or isn't valid, then
623             # reset it back to the default, which is the time of the file's
624             # first commit.
625             if (!defined $time) {
626             my $guid_id = db_select($db, wc_file => $id, 'guid_id');
627             ($time, undef) = guid_first_last_times($db, $guid_id);
628             assert(defined $time) if DEBUG;
629             }
630             $update{issued_at} = db_datetime($time);
631             }
632              
633             if (exists $props->{'dcterms:modified'}) {
634             my $time = validate_date($props->{'dcterms:modified'});
635             warn "file $id has invalid 'dcterms:modified' datetime, ignoring\n"
636             if !defined $time && defined $props->{'dcterms:modified'};
637             # If the custom update datetime is removed, or isn't valid, then
638             # reset it back to the default, which is the time of the file's
639             # most recent commit.
640             if (!defined $time) {
641             my $guid_id = db_select($db, wc_file => $id, 'guid_id');
642             (undef, $time) = guid_first_last_times($db, $guid_id);
643             assert(defined $time) if DEBUG;
644             }
645             $update{modified_at} = db_datetime($time);
646             }
647              
648             while (my ($property, $column) = each %Daizu::OVERRIDABLE_PROPERTY) {
649             $update{$column} = trim_with_empty_null($props->{$property})
650             if exists $props->{$property};
651             }
652              
653             if (exists $props->{'daizu:flags'}) {
654             my @stat = split ' ', $props->{'daizu:flags'};
655             $update{retired} = $update{no_index} = 0;
656             for (@stat) {
657             if ($_ eq 'retired') {
658             $update{retired} = 1;
659             }
660             elsif ($_ eq 'no-index') {
661             $update{no_index} = 1;
662             }
663             else {
664             warn "file contains unrecognized value '$_' in 'daizu:flags'";
665             }
666             }
667             }
668              
669             $update{custom_url} = validate_uri($props->{'daizu:url'})
670             if exists $props->{'daizu:url'};
671              
672             db_update $db, wc_file => $id, %update;
673              
674             if (exists $props->{'daizu:tags'}) {
675             db_delete($db, 'wc_file_tag', file_id => $id);
676             if (defined $props->{'daizu:tags'}) {
677             for (split /\s*[\x0A\x0D]\s*/, trim($props->{'daizu:tags'})) {
678             my $original = $_;
679             # There is no standard for how tags should be written and
680             # what characters are allowed. I fold them to lowercase, and
681             # collapse sequences of whitespace to a single space.
682             $_ = lc $_;
683             s/\s+/ /g;
684             db_insert($db, 'tag', tag => $_)
685             unless db_row_exists($db, 'tag', tag => $_);
686             db_insert($db, 'wc_file_tag',
687             file_id => $id,
688             tag => $_,
689             original_spelling => $original,
690             );
691             }
692             }
693             }
694             }
695              
696             =item $cms-Ecall_property_loaders($id, $props)
697              
698             Calls the plugin methods which wish to be informed of property changes on
699             a file, where C<$id> is a file ID for a record in the C table,
700             and C<$props> is a reference to a hash of the format described for the
701             Ladd_property_loader($pattern, $object, $method)>
702             method.
703              
704             =cut
705              
706             sub call_property_loaders
707             {
708             my ($self, $id, $props) = @_;
709             my $loaders = $self->{property_loaders};
710              
711             my %seen_loader;
712             my %seen_prefix;
713             for my $name (keys %$props) {
714             if (exists $loaders->{$name}) {
715             for my $loader (@{$loaders->{$name}}) {
716             next if exists $seen_loader{"$loader"};
717             my ($object, $method) = @$loader;
718             $object->$method($self, $id, $props);
719             undef $seen_loader{"$loader"};
720             }
721             }
722              
723             if ($name =~ /^([^:]+):/ && !$seen_prefix{$1} &&
724             exists $loaders->{"$1:*"})
725             {
726             undef $seen_prefix{$1};
727             for my $loader (@{$loaders->{"$1:*"}}) {
728             next if exists $seen_loader{"$loader"};
729             my ($object, $method) = @$loader;
730             $object->$method($self, $id, $props);
731             undef $seen_loader{"$loader"};
732             }
733             }
734             }
735              
736             if (exists $loaders->{'*'}) {
737             for my $loader (@{$loaders->{'*'}}) {
738             next if exists $seen_loader{"$loader"};
739             my ($object, $method) = @$loader;
740             $object->$method($self, $id, $props);
741             undef $seen_loader{"$loader"};
742             }
743             }
744             }
745              
746             =item $cms-Eguid_entity
747              
748             Return the entity to be used for minting GUID URLs for the file at
749             C<$path>. This finds the best match from the C elements
750             in the configuration file and returns the corresponding C value.
751              
752             =cut
753              
754             sub guid_entity
755             {
756             my ($self, $path) = @_;
757             my $best_entity = $self->{default_entity};
758             my $matched_path = '';
759              
760             while (my ($want_path, $entity) = each %{$self->{path_entity}}) {
761             next if length($matched_path) > length($want_path);
762             next unless $path eq $want_path ||
763             substr($path, 0, length($want_path) + 1) eq "$want_path/";
764             $best_entity = $entity;
765             $matched_path = $want_path;
766             }
767              
768             return $best_entity;
769             }
770              
771             =item $cms-Eoutput_config($url)
772              
773             Return information about where the published output for C<$url> (a
774             string or L object) should be written to. If there is a suitable
775             C element in the configuration file then this will return a hash
776             containing information from that element, followed by a list
777             of three strings, which will all be defined. If you join these strings
778             together (by passing them to the C function from L for
779             example) to form a complete path then it will be the path to the file
780             (never directory) which the output should be written to.
781              
782             The first value returned will be a reference to a hash containing the
783             following keys:
784              
785             =over
786              
787             =item url
788              
789             The value from the C attribute in the configuration file, as
790             a L object.
791              
792             =item path
793              
794             The value from the C attribute.
795              
796             =item index_filename
797              
798             The value from the C attribute, or the default
799             value I if one isn't set.
800              
801             =item redirect_map
802              
803             The value from the C attribute, or undef if there isn't one.
804              
805             =item gone_map
806              
807             The value from the C attribute, or undef if there isn't one.
808              
809             =back
810              
811             The other three values are:
812              
813             =over
814              
815             =item *
816              
817             The absolute path to the document root directory, which will be the value
818             of the C attribute in the appropriate C element in the
819             configuration file. This is the same as the C value in the hash.
820              
821             =item *
822              
823             The relative path from there to the directory in which the output file
824             should be written. This is given separately so that you can create that
825             directory if it doesn't exist. This will be the empty string if the
826             output file is to be stored directly in the document root directory, but
827             the C function mentioned above will correctly elide it for you in
828             that case.
829              
830             =item *
831              
832             The filename of the output file. This is a single name, not a path.
833              
834             =back
835              
836             If the configuration doesn't say where C<$url> should be published to then
837             this will return nothing.
838              
839             TODO - this doesn't use C itself, so the results aren't portable
840             across different platforms.
841              
842             =cut
843              
844             sub output_config
845             {
846             my ($self, $out_url) = @_;
847             $out_url = URI->new($out_url) unless ref $out_url;
848              
849             # Search through all the configured output URLs in reverse order to
850             # find the most specific (longest) one which is a prefix of $out_url.
851             # We do that by checking to see if $out_url can be expressed relative to
852             # the output's base URL without going backwards with '../' at the start.
853             my ($config, $path);
854             for my $url (sort { length $b <=> length $a } keys %{$self->{output}}) {
855             my $rel_url = $out_url->rel($url);
856             next if $rel_url eq $out_url;
857             $rel_url = '' if $rel_url eq './';
858             next if $rel_url =~ m!^\.\.?(?:/|$)!;
859             $config = $self->{output}{$url};
860             $path = $rel_url;
861             last;
862             }
863              
864             return unless defined $config;
865              
866             my $filename = $config->{index_filename};
867             $filename = $1
868             if $path =~ m!(?:^|/)([^/]+)\z!;
869             $path =~ s!(?:^|/)[^/]*\z!!;
870              
871             return ($config, $config->{path}, $path, $filename);
872             }
873              
874             =back
875              
876             =head1 COPYRIGHT
877              
878             This software is copyright 2006 Geoff Richards Egeoff@laxan.comE.
879             For licensing information see this page:
880              
881             L
882              
883             =cut
884              
885             1;
886             # vi:ts=4 sw=4 expandtab