# Copyright 2001, 2002 Benjamin Trott. This code cannot be redistributed without # permission from www.movabletype.org. # # $Id: MT.pm,v 1.79 2002/05/04 05:32:37 btrott Exp $ package MT; use strict; use vars qw( $VERSION ); $VERSION = '2.11'; use MT::ConfigMgr; use MT::Object; use MT::Blog; use MT::Util qw( start_end_day start_end_week start_end_month archive_file_for get_entry ); use File::Spec; use Fcntl qw( LOCK_EX ); use MT::ErrorHandler; @MT::ISA = qw( MT::ErrorHandler ); sub version_slug { return <init(@_) or return $class->error($mt->errstr); $mt; } sub init { my $mt = shift; my %param = @_; my $cfg = $mt->{cfg} = MT::ConfigMgr->instance; my($cfg_file); unless ($cfg_file = $param{Config}) { for my $f (qw( mt.cfg )) { $cfg_file = $f, last if -r $f; } } if ($cfg_file) { $cfg->read_config($cfg_file) or return $mt->error($cfg->errstr); } MT::Object->set_driver($cfg->ObjectDriver); $mt->{__rebuilt} = {}; $mt->{__cached_maps} = {}; $mt->{__cached_templates} = {}; $mt; } sub rebuild { my $mt = shift; my %param = @_; my $blog; unless ($blog = $param{Blog}) { my $blog_id = $param{BlogID}; $blog = MT::Blog->load($blog_id) or return $mt->error("Load of blog '$blog_id' failed: " . MT::Blog->errstr); } my $at = $blog->archive_type || ''; my @at = split /,/, $at; if (my $set_at = $param{ArchiveType}) { my %at = map { $_ => 1 } @at; return $mt->error("Archive type '$set_at' is not a chosen archive type") unless $at{$set_at}; @at = ($set_at); } if (@at) { require MT::Entry; my %arg = ('sort' => 'created_on', direction => 'descend'); if ($param{Limit}) { $arg{offset} = $param{Offset}; $arg{limit} = $param{Limit}; } my $iter = MT::Entry->load_iter({ blog_id => $blog->id, status => MT::Entry::RELEASE() }, \%arg); my $cb = $param{EntryCallback}; while (my $entry = $iter->()) { $cb->($entry) if $cb; for my $at (@at) { if ($at eq 'Category') { my $cats = $entry->categories; for my $cat (@$cats) { $mt->_rebuild_entry_archive_type( Entry => $entry, Blog => $blog, Category => $cat, ArchiveType => 'Category' ) or return; } } else { $mt->_rebuild_entry_archive_type( Entry => $entry, Blog => $blog, ArchiveType => $at ) or return; } } } } unless ($param{NoIndexes}) { $mt->rebuild_indexes( Blog => $blog ) or return; } 1; } sub rebuild_entry { my $mt = shift; my %param = @_; my $entry = $param{Entry} or return $mt->error("Parameter 'Entry' is required"); my $blog; unless ($blog = $param{Blog}) { my $blog_id = $entry->blog_id; $blog = MT::Blog->load($blog_id) or return $mt->error("Load of blog '$blog_id' failed: " . MT::Blog->errstr); } my $at = $blog->archive_type; if ($at && $at ne 'None') { my @at = split /,/, $at; for my $at (@at) { if ($at eq 'Category') { my $cats = $entry->categories; for my $cat (@$cats) { $mt->_rebuild_entry_archive_type( Entry => $entry, Blog => $blog, ArchiveType => $at, Category => $cat, ) or return; } } else { $mt->_rebuild_entry_archive_type( Entry => $entry, Blog => $blog, ArchiveType => $at ) or return; } } } ## The above will just rebuild the archive pages for this particular ## entry. If we want to rebuild all of the entries/archives/indexes ## on which this entry could be featured etc., however, we need to ## rebuild all of the entry's dependencies. Note that all of these ## are not *necessarily* dependencies, depending on the usage of tags, ## etc. There is not a good way to determine exact dependencies; it is ## easier to just rebuild, rebuild, rebuild. return 1 unless $param{BuildDependencies}; ## Rebuild previous and next entry archive pages. if (my $prev = $entry->previous) { $mt->rebuild_entry( Entry => $prev ) or return; } if (my $next = $entry->next) { $mt->rebuild_entry( Entry => $next ) or return; } ## Rebuild all indexes, in case this entry is on an index. $mt->rebuild_indexes( Blog => $blog ) or return; ## Rebuild previous and next daily, weekly, and monthly archives; ## adding a new entry could cause changes to the intra-archive ## navigation. my %at = map { $_ => 1 } split /,/, $blog->archive_type; for my $at (qw( Daily Weekly Monthly )) { if ($at{$at}) { my @arg = ($entry->created_on, $entry->blog_id, $at); if (my $prev_arch = get_entry(@arg, 'previous')) { $mt->_rebuild_entry_archive_type( Entry => $prev_arch, Blog => $blog, ArchiveType => $at) or return; } if (my $next_arch = get_entry(@arg, 'next')) { $mt->_rebuild_entry_archive_type( Entry => $next_arch, Blog => $blog, ArchiveType => $at) or return; } } } 1; } sub _rebuild_entry_archive_type { my $mt = shift; my %param = @_; my $at = $param{ArchiveType} or return $mt->error("Parameter 'ArchiveType' is required"); return 1 if $at eq 'None'; my $entry = $param{Entry} or return $mt->error("Parameter 'Entry' is required"); my $blog; unless ($blog = $param{Blog}) { my $blog_id = $entry->blog_id; $blog = MT::Blog->load($blog_id) or return $mt->error("Load of blog '$blog_id' failed: " . MT::Blog->errstr); } ## Load the template-archive-type map entries for this blog and ## archive type. We do this before we load the list of entries, because ## we will run through the files and check if we even need to rebuild ## anything. If there is nothing to rebuild at all for this entry, ## we save some time by not loading the list of entries. require MT::TemplateMap; my @map; if (my $maps = $mt->{__cached_maps}{$at . $blog->id}) { @map = @$maps; } else { @map = MT::TemplateMap->load({ archive_type => $at, blog_id => $blog->id }); $mt->{__cached_maps}{$at . $blog->id} = \@map; } return $mt->error("You selected the archive type '$at', but you did not " . "define a template for this archive type.") unless @map; my @map_build; ## We keep a running total of the pages we have rebuilt ## in this session in $mt->{__rebuilt}. my $done = $mt->{__rebuilt}; for my $map (@map) { my $file = archive_file_for($entry, $blog, $at, $param{Category}, $map); push @map_build, $map unless $done->{$file}; $map->{__saved_output_file} = $file; } return 1 unless @map_build; @map = @map_build; my(%cond); require MT::Template::Context; my $ctx = MT::Template::Context->new; $ctx->{current_archive_type} = $at; if ($at eq 'Individual') { $ctx->stash('entry', $entry); $ctx->{current_timestamp} = $entry->created_on; $cond{EntryIfAllowComments} = $entry->allow_comments; $cond{EntryIfExtended} = $entry->text_more ? 1 : 0; } elsif ($at eq 'Daily') { my($start, $end) = start_end_day($entry->created_on, $blog); $ctx->{current_timestamp} = $start; $ctx->{current_timestamp_end} = $end; my @entries = MT::Entry->load({ created_on => [ $start, $end ], blog_id => $blog->id, status => MT::Entry::RELEASE() }, { range => { created_on => 1 } }); $ctx->stash('entries', \@entries); } elsif ($at eq 'Weekly') { my($start, $end) = start_end_week($entry->created_on, $blog); $ctx->{current_timestamp} = $start; $ctx->{current_timestamp_end} = $end; my @entries = MT::Entry->load({ created_on => [ $start, $end ], blog_id => $blog->id, status => MT::Entry::RELEASE() }, { range => { created_on => 1 } }); $ctx->stash('entries', \@entries); } elsif ($at eq 'Monthly') { my($start, $end) = start_end_month($entry->created_on, $blog); $ctx->{current_timestamp} = $start; $ctx->{current_timestamp_end} = $end; my @entries = MT::Entry->load({ created_on => [ $start, $end ], blog_id => $blog->id, status => MT::Entry::RELEASE() }, { range => { created_on => 1 } }); $ctx->stash('entries', \@entries); } elsif ($at eq 'Category') { my $cat; unless ($cat = $param{Category}) { return $mt->error("Building category archives, but no category " . "provided."); } require MT::Placement; $ctx->stash('archive_category', $cat); my @entries = MT::Entry->load({ blog_id => $blog->id, status => MT::Entry::RELEASE() }, { 'join' => [ 'MT::Placement', 'entry_id', { category_id => $cat->id } ] }); $ctx->stash('entries', \@entries); } my $fmgr = $blog->file_mgr; my $arch_root = $blog->archive_path; return $mt->error("You did not set your Local Archive Path") unless $arch_root; ## For each mapping, we need to rebuild the entries we loaded above in ## the particular template map, and write it to the specified archive ## file template. require MT::Template; for my $map (@map) { my $file = File::Spec->catfile($arch_root, $map->{__saved_output_file}); my $tmpl = $mt->{__cached_templates}{$map->template_id}; unless ($tmpl) { $tmpl = MT::Template->load($map->template_id); if ($mt->{cache_templates}) { $mt->{__cached_templates}{$tmpl->id} = $tmpl; } } my $html = $tmpl->build($ctx, \%cond) or return $mt->error("Build entry '" . $entry->title . "' failed: " . $tmpl->errstr); ## Untaint. We have to assume that we can trust the user's setting of ## the archive_path, and nothing else is based on user input. ($file) = $file =~ /(.+)/s; ## Determine if we need to build directory structure, and build it ## if we do. DirUmask determines directory permissions. my($vol, $path, $fname) = File::Spec->splitpath($file); $path =~ s!/$!!; ## OS X doesn't like / at the end in mkdir(). unless ($fmgr->exists($path)) { $fmgr->mkpath($path) or return $mt->error("Error making path '$path': " . $fmgr->errstr); } ## By default we write all data to temp files, then rename the temp ## files to the real files (an atomic operation). Some users don't ## like this (requires too liberal directory permissions). So we ## have a config option to turn it off (NoTempFiles). my $use_temp_files = !$mt->{cfg}->NoTempFiles; my $temp_file = $use_temp_files ? "$file.new" : $file; defined($fmgr->put_data($html, $temp_file)) or return $mt->error("Writing to '$temp_file' failed: " . $fmgr->errstr); if ($use_temp_files) { $fmgr->rename($temp_file, $file) or return $mt->error("Renaming tempfile '$temp_file' failed: " . $fmgr->errstr); } $done->{$map->{__saved_output_file}}++; } 1; } sub rebuild_indexes { my $mt = shift; my %param = @_; require MT::Template; require MT::Template::Context; require MT::Entry; my $blog; unless ($blog = $param{Blog}) { my $blog_id = $param{BlogID}; $blog = MT::Blog->load($blog_id) or return $mt->error("Load of blog '$blog_id' failed: " . MT::Blog->errstr); } my $iter; if (my $tmpl = $param{Template}) { my $i = 0; $iter = sub { $i++ < 1 ? $tmpl : undef }; } else { $iter = MT::Template->load_iter({ type => 'index', blog_id => $blog->id }); } local *FH; my $site_root = $blog->site_path; return $mt->error("You did not set your Local Site Path") unless $site_root; my $fmgr = $blog->file_mgr; while (my $tmpl = $iter->()) { ## Skip index templates that the user has designated not to be ## rebuilt automatically. We need to do the defined-ness check ## because we added the flag in 2.01, and for templates saved ## before that time, the rebuild_me flag will be undefined. But ## we assume that these templates should be rebuilt, since that ## was the previous behavior. next if !$param{Force} && defined $tmpl->rebuild_me && !$tmpl->rebuild_me; my $ctx = MT::Template::Context->new; my $html = $tmpl->build($ctx); return $mt->error( $tmpl->errstr ) unless defined $html; my $index = $tmpl->outfile or return $mt->error("Template '" . $tmpl->name . "' does not " . "have an Output File."); unless (File::Spec->file_name_is_absolute($index)) { $index = File::Spec->catfile($site_root, $index); } ## Untaint. We have to assume that we can trust the user's setting of ## the site_path and the template outfile. ($index) = $index =~ /(.+)/s; my $use_temp_files = !$mt->{cfg}->NoTempFiles; my $temp_file = $use_temp_files ? "$index.new" : $index; defined($fmgr->put_data($html, $temp_file)) or return $mt->error("Writing to '$temp_file' failed: " . $fmgr->errstr); if ($use_temp_files) { $fmgr->rename($temp_file, $index) or return $mt->error("Renaming tempfile '$temp_file' " . "failed: " . $fmgr->errstr); } } 1; } sub ping { my $mt = shift; my %param = @_; my $blog; unless ($blog = $param{Blog}) { my $blog_id = $param{BlogID}; $blog = MT::Blog->load($blog_id) or return $mt->error("Load of blog '$blog_id' failed: " . MT::Blog->errstr); } if ($blog->ping_weblogs) { require MT::XMLRPC; MT::XMLRPC->weblogs_ping($blog) or return $mt->error(MT::XMLRPC->errstr); } if ($blog->mt_update_key) { require MT::XMLRPC; MT::XMLRPC->mt_ping($blog) or return $mt->error(MT::XMLRPC->errstr); } ## xxx now we should allow people to register generic callbacks ## in some fashion; maybe by putting code into a directory? 1; } 1; __END__ =head1 NAME MT - Movable Type =head1 SYNOPSIS use MT; my $mt = MT->new; $mt->rebuild(BlogID => 1) or die $mt->errstr; =head1 DESCRIPTION The I class is the main high-level rebuilding/pinging interface in the Movable Type library. It handles all rebuilding operations. It does B handle any of the application functionality--for that, look to I and I, both of which subclass I to handle application requests. =head1 USAGE I has the following interface. On failure, all methods return C and set the I for the object or class (depending on whether the method is an object or class method, respectively); look below at the section L for more information. =head2 MT->new( %args ) Constructs a new I instance and returns that object. Returns C on failure. I will also read your F file (provided that it can find it--if you find that it can't, take a look at the I directive, below). It will also initialize the chosen object driver; the default is the C object driver. I<%args> can contain: =over 4 =item * Config Path to the F file. If you do not specify a path, I will try to find your F file in the current working directory. =back =head2 $mt->rebuild( %args ) Rebuilds your entire blog, indexes and archives; or some subset of your blog, as specified in the arguments. I<%args> can contain: =over 4 =item * Blog An I object corresponding to the blog that you would like to rebuild. Either this or C is required. =item * BlogID The ID of the blog that you would like to rebuild. Either this or C is required. =item * ArchiveType The archive type that you would like to rebuild. This should be one of the following values: C, C, C, C, or C. This argument is optional; if not provided, all archive types will be rebuilt. =item * EntryCallback A callback that will be called for each entry that is rebuilt. If provided, the value should be a subroutine reference; the subroutine will be handed the I object for the entry that is about to be rebuilt. You could use this to keep a running log of which entry is being rebuilt, for example: $mt->rebuild( BlogID => $blog_id, EntryCallback => sub { print $_[0]->title, "\n" }, ); Or to provide a status indicator: use MT::Entry; my $total = MT::Entry->count({ blog_id => $blog_id }); my $i = 0; local $| = 1; $mt->rebuild( BlogID => $blog_id, EntryCallback => sub { printf "%d/%d\r", ++$i, $total }, ); print "\n"; This argument is optional; by default no callbacks are executed. =item * NoIndexes By default I will rebuild the index templates after rebuilding all of the entries; if you do not want to rebuild the index templates, set the value for this argument to a true value. This argument is optional. =item * Limit Limit the number of entries to be rebuilt to the last C entries in the blog. For example, if you set this to C<20> and do not provide an offset (see L, below), the 20 most recent entries in the blog will be rebuilt. This is only useful if you are rebuilding C archives. This argument is optional; by default all entries will be rebuilt. =item * Offset When used with C, specifies the entry at which to start rebuilding your individual entry archives. For example, if you set this to C<10>, and set a C of C<5> (see L, above), entries 10-14 (inclusive) will be rebuilt. The offset starts at C<0>, and the ordering is reverse chronological. This is only useful if you are rebuilding C archives, and if you are using C. This argument is optional; by default all entries will be rebuilt, starting at the first entry. =back =head2 $mt->rebuild_entry( %args ) Rebuilds a particular entry in your blog (and its dependencies, if specified). I<%args> can contain: =over 4 =item * Entry An I object corresponding to the object you would like to rebuild. This argument is required. =item * Blog An I object corresponding to the blog to which the I belongs. This argument is optional; if not provided, the I object will be loaded in I from the I<$entry-Eblog_id> column of the I object passed in. If you already have the I object loaded, however, it makes sense to pass it in yourself, as it will skip one small step in I (loading the object). =item * BuildDependencies Saving an entry can have effects on other entries; so after saving, it is often necessary to rebuild other entries, to reflect the changes onto all of the affected archive pages, indexes, etc. If you supply this parameter with a true value, I will rebuild: the archives for the next and previous entries, chronologically; all of the index templates; the archives for the next and previous daily, weekly, and monthly archives. =back =head2 $mt->rebuild_indexes( %args ) Rebuilds all of the index templates in your blog, or just one, if you use the I