# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Site.pm 3232 2008-08-21 11:42:36Z josh $

package BigMed::Site;
use warnings;
use strict;
use utf8;
use Carp;
use base qw(BigMed::Data);
use BigMed;
use BigMed::Prefs;
use BigMed::Section;
use BigMed::DiskUtil qw(bm_file_path bm_delete_file bm_delete_dir bm_move_file bm_move_dir bm_confirm_dir);
use English qw(-no_match_vars);

###########################################################
# SET SITE DATA SCHEMA
###########################################################

my @site_schema = (
    {   name    => 'name',
        type    => 'simple_text',
        index   => 1,
        unique  => 1,
        default => q{},
    },
    {   name    => 'html_dir',
        type    => 'dir_path',
        index   => 1,
        default => q{},
        unique  => 1,
    },
    {   name    => 'homepage_dir',
        type    => 'dir_path',
        index   => 1,
        default => q{},
        unique  => 1,
    },
    {   name    => 'html_url',
        type    => 'dir_url',
        default => q{},
    },
    {   name    => 'homepage_url',
        type    => 'dir_url',
        default => q{},
    },
    {   name    => 'flags',
        type    => 'key_boolean',
        default => {},
    },
    {   name    => 'site_doclimit',
        type    => 'kilobytes',
        default => 5120,
    },
    {   name => 'time_offset',
        type => 'time_offset',
    },
    {   name    => 'date_format',
        type    => 'raw_text',
        default => '%b %e, %Y',
    },
    {   name    => 'time_format',
        type    => 'raw_text',
        default => '%r',
    },
);

BigMed::Site->set_schema(
    source     => 'sites',
    label      => 'site',
    elements   => \@site_schema,
    systemwide => 1
);

###########################################################
# ADDITIONAL ACCESSORS
###########################################################

#these return values that are dependent on other stored
#values

sub image_dir_path { #legacy, deprecated
    return $_[0]->image_path();
}

sub image_url {
    my $hurl = $_[0]->html_url or return;
    my $dot = BigMed->bigmed->env('DOT');
    return "$hurl/bm${dot}pix";
}

sub image_path {
    my $hdir = $_[0]->html_dir or return;
    my $dot = BigMed->bigmed->env('DOT');
    return bm_file_path( $hdir, "bm${dot}pix" );
}

sub doc_url {
    my $hurl = $_[0]->html_url or return;
    my $dot = BigMed->bigmed->env('DOT');
    return "$hurl/bm${dot}doc";
}

sub doc_path {
    my $hdir = $_[0]->html_dir or return;
    my $dot = BigMed->bigmed->env('DOT');
    return bm_file_path( $hdir, "bm${dot}doc" );
}

###########################################################
# DATA CALLBACKS
###########################################################

BigMed::Site->add_callback( 'before_trash', \&_before_trash );
BigMed::Site->add_callback( 'before_trash_all', \&_before_trash_all );


###########################################################
# PREFERENCE STORAGE, CACHING AND RETRIEVAL
###########################################################

#the first time that a preference is stored, retrieved,
#or cleared, all preferences for the site are loaded and
#cached into the site object for fast retrieval and
#easy tree climbing to find inherited values.

#the store_pref, get_pref_value and clear_pref methods
#update both the in-memory cache and the disk-based pref
#so that the cache and the disk are always in sync.

sub store_pref {
    my $site = shift;
    my $name = shift
      or croak 'No preference name provided in store_pref request';
    my $value   = shift;
    my $section = shift || 0;

    my $site_id = $site->id
      or croak 'Site must have an id before storing preference data';
    BigMed::Prefs->pref_exists($name)
      or croak "No preference '$name' is registered (have you "
      . 'required the appropriate Format.pm subclass?)';

    #section must be known to site
    my $unknown_to_site;
    if ( ref $section ) {
        $unknown_to_site = ( $section->site != $site->id );
        $section = $section->id;    #use id from here on out...
    }
    elsif ($section) {
        $unknown_to_site = !$site->section_obj_by_id($section);
    }

    if ($unknown_to_site) {
        return $site->set_error(
            head => 'SITE_HEAD_Could_not_save_pref',
            text => [
                'SITE_Could_not_save_pref',
                $section,
                ( $site->name || 'Untitled' ),
                BigMed->bigmed->env('ADMINEMAIL')
            ],
        );
    }

    if ( !defined $value ) {    #remove value from data store
        return $site->clear_pref( $name, $section );
    }

    #massage the pref value into an array reference because
    #the pref_value attribute of the Prefs data object
    #is stored in an array format.
    my $value_array;
    if ( ref $value eq 'HASH' ) {
        $value_array = [%{$value}];
    }
    elsif ( ref $value eq 'ARRAY' ) {
        $value_array = $value;
    }
    else {
        $value_array = [$value];
    }

    ### Start the process of saving to disk

    #get any existing preference objects
    #(should only be one, but just in case, get all)
    my @pref_objects = BigMed::Prefs->fetch(
        {   site    => $site_id,
            section => $section,
            name    => $name
        }
    );
    return if $site->error;
    my $pref_obj = shift @pref_objects;
    
    #if there are extras, kill 'em
    foreach my $extra(@pref_objects) {
        $extra->trash or return;
    }

    #if there's no existing preference for this site/section,
    #create a new one
    if ( !$pref_obj ) {
        $pref_obj = BigMed::Prefs->new;
        $pref_obj->set_site($site_id);
        $pref_obj->set_section($section);
        $pref_obj->set_name($name);
    }

    #set the value and save to disk
    $pref_obj->set_pref_value($value_array);
    $pref_obj->save or return;

    #update the in-memory pref cache
    my $rprefs = $site->ref_to_preference_hash() or return;
    $rprefs->{$name}->{$section} = $value_array;

    return 1;
}

sub get_pref_value {

    #returns the *effective* preference. That is, if the specified
    #section has no preference, then the appropriate inherited
    #value is returned, all the way up to the site-wide (section=0)
    #preference. If no site-wide preference is specified,
    #the value for the fallback widget, if any, is returned, or
    #the preference's default value.

    #note re error trapping: If there's an error retrieving the
    #preference, the method will return undef for scalars and
    #an empty array for arrays and hashes. So requests for arrays
    #and hashes should check for errors before continuing if they
    #receive an empty set.

    my $site = shift;
    $site->id
      or croak 'Site must have an id before getting preference data';
    my $name = shift
      or croak 'No preference name provided in get_pref_value request';
    my $section = shift || 0;
    my $pref    = shift || {};
    $section = $section->id if ref $section;

    BigMed::Prefs->pref_exists($name)
      or croak "No preference '$name' is registered (have you "
      . 'loaded the appropriate Format.pm subclass?)';

    #retrieve the preference hash; handle errors carefully, because
    #we can't just blindly return an undef value (could confuse
    #hash- and array-format preferences).

    my $rprefs = $site->ref_to_preference_hash();
    my @value;
    if ( !defined $rprefs ) {    #encountered an error
        @value = (undef);
    }
    else {    #got the hash, get the value from memory; get inherited
              #value if it doesn't exist
        my $rpref_value =
          $site->_pref_effective_value( $rprefs, $name, $section );

        if ( !defined $rpref_value ) {    #no pref, no inheritance, no default
            my $fallback = BigMed::Prefs->pref_fallback($name)
              or croak "No default value specified for widget pref '$name'";
            my @value = $site->get_pref_value( $fallback, $section );

            #handle any error
            $rpref_value = ( @value == 0 && $site->error ) ? undef: \@value;
        }

        #should never have an undefined value;
        defined $rpref_value
          or croak "No default value defined for widget preference '$name'";

        #nudge into array reference
        if ( ref $rpref_value eq 'HASH' ) {
            $rpref_value = [%{$rpref_value}];
        }
        elsif ( !ref $rpref_value ) {
            $rpref_value = [$rpref_value];
        }

        @value = @{$rpref_value};
    }

    #now @value either holds the preference value array, or
    #a single-item array that holds undef, indicating an error.

    #because prefs are always stored as arrays, need to give a little
    #help for empty strings and undefined values.
    $value[0] = q{} if @value == 0 && !wantarray;
    @value = () if @value == 1 && !defined $value[0];
    return wantarray ? @value : $value[0];
}

sub _pref_effective_value {
    my ( $site, $rprefs, $name, $section ) = @_;
    my $rpref_value = $rprefs->{$name}->{$section};
    if ( !defined $rpref_value && $section > 0 ) {

        #looking for a section pref (not a site-wide pref),
        #and it's not defined; walk the parents to find the
        #next value to inherit

        my $section_obj = $site->section_obj_by_id($section);
        if ($section_obj) {    #got the object
                #go through the section's parents to find the pref
                #go back thru parents, immediate parents first
            my @upward_parents = reverse $section_obj->parents;

            #but don't use the homepage
            if (   $upward_parents[-1]
                && $upward_parents[-1] == $site->homepage_id )
            {

                #pop off the homepage id and add 0 to force
                #check of sitewide prefs if no section-specific
                #prefs available
                $upward_parents[-1] = '0';
            }
            foreach my $parent (@upward_parents) {
                last
                  if defined( $rpref_value = $rprefs->{$name}->{$parent} );
            }
        }
    }

    #use the default if no preference exists up the tree, including
    #in site-wide preference (or if the section just doesn't exist)
    $rpref_value = BigMed::Prefs->pref_default($name)
      if !defined $rpref_value;

    return $rpref_value;
}

sub clear_pref {
    my $site    = shift;
    my $site_id = $site->id
      or croak 'Site must have an id before clearing preference data';
    my $name = shift
      or croak 'No preference name provided in clear_pref request';
    my $section = shift || 0;

    BigMed::Prefs->pref_exists($name)
      or croak "No preference '$name' is registered (have you "
      . 'required the appropriate Format.pm subclass?)';

    #clear the pref value from disk
    my $selection = BigMed::Prefs->select(
        {   site    => $site_id,
            section => $section,
            name    => $name
        }
      )
      or return;
    $selection->trash_all or return;

    #clear the pref value from memory
    my $rprefs = $site->ref_to_preference_hash() or return;
    delete $rprefs->{$name}->{$section};

    return 1;
}

sub ref_to_preference_hash {    #PRIVATE INTERNAL METHOD; COULD CHANGE
    my $site = shift;
    return $site->stash('pref') if defined $site->stash('pref');

    #haven't loaded the preferences yet, do it now...
    my $site_id = $site->id
      or croak 'Site must have an id before loading preference data';
    my $all_prefs = BigMed::Prefs->select( { site => $site_id } );

    #step through all prefs and stow in a hash indexed by pref name
    #and section id
    my $pref;
    my %pref_cache;
    while ( $pref = $all_prefs->next ) {
        $pref_cache{ $pref->name }->{ $pref->section } = [$pref->pref_value];
    }

    #handle error if there is one
    defined $pref or return;

    #put the whole pref_cache into the stash
    $site->set_stash( 'pref' => \%pref_cache );

    #return the hash reference
    return \%pref_cache;
}

sub pref_inherits_from_fallback {
    my ( $site, $name, $section ) = @_;
    $section ||= 0;
    my $rprefs = $site->ref_to_preference_hash();

    #if there's no defined effective value, that means the preference
    #does not have its own defined value and inherits from a fallback pref
    return
      defined( $site->_pref_effective_value( $rprefs, $name, $section ) )
      ? 0
      : 1;
}

###########################################################
# SECTION MANAGEMENT
###########################################################

sub load_sections {

    #returns true if all sections loaded into the cache
    #returns undef if there was an error loading the sections
    #(check error queue in that case).

    #overwrites the existing cache if called a second time

    my $site = shift;
    my %section;
    my %slug;

    $site->id
      or croak 'site needs id for load_sections '
      . '(try saving the site object first)';

    #clear the homepage, section and slug stash to start fresh
    $site->set_stash(
        section  => undef,
        slug     => undef,
        homepage => undef,
    );

    #call up all sections for the site
    my $all_sections = BigMed::Section->select( { site => $site->id } )
      or return;

    #step through all of the sections and load into the hashes
    my $section;
    while ( $section = $all_sections->next ) {
        my $id = $section->id;
        $section{$id} = $section;
        $slug{ $section->slug } = $id if $section->slug;
        $site->set_stash( 'homepage' => $id ) if $section->is_homepage;
    }
    defined $section or return;    #error if undef

    #put the hashes into the stash
    $site->set_stash(
        'section' => \%section,
        'slug'    => \%slug,
    );
    return 1;
}

sub section_obj_by_id {

    #an empty return value indicates no such section exists in site;
    #an undef value indicates that there was an error retrieving the
    #section (check the error queue).

    my $site       = shift;
    my $section_id = shift
      or croak 'section_obj_by_id request requires section id';

    #get the hash reference for the section cache (return if error)
    my $rsections = $site->ref_to_section_hash() or return;

    #return the entry for this specific id
    return $rsections->{$section_id} || q{};
}

sub section_obj_by_slug {

    #an empty return value indicates no such section exists in site;
    #an undef value indicates that there was an error retrieving the
    #section (check the error queue).

    my $site      = shift;
    my $slug_name = shift
      or croak 'section_obj_by_slug request requires slug name';

    #get the hash reference for the section cache (return if error)
    my $rslugs = $site->ref_to_slug_hash() or return;

    my $section_id = $rslugs->{$slug_name};
    return $section_id ? $site->section_obj_by_id($section_id) : q{};
}

sub add_section {

    #returns the section object on success;
    #returns undef if there's an error, and adds error message to error queue

    my $site    = shift;
    my $section = shift;
    my $site_id = $site->id
      or croak 'Site id required to add a section (try saving the site)';
    if ( !ref $section || !$section->isa('BigMed::Section') ) {
        $section = BigMed::Section->new();
    }
    $section->set_site( $site->id );
    $section->id or $section->update_id or return;

    #add to section and slug maps
    my $rsections = $site->ref_to_section_hash();
    $rsections->{ $section->id } = $section;
    if ( $section->slug ) {
        my $rslugs = $site->ref_to_slug_hash();
        $rslugs->{ $section->slug } = $section->id;
    }
    $site->set_stash( 'homepage' => $section->id ) if $section->is_homepage;

    return $section;
}

sub homepage_id {

    # $site->homepage_id()
    # returns the id of the site's homepage section.
    # if no homepage is found, returns an empty string ''
    # if an error is encountered in loading the info, returns undef

    my $site        = shift;
    my $homepage_id = $site->stash('homepage');
    return $homepage_id if defined $homepage_id;

    #no homepage_id is stored; see if sections are loaded and load
    #them if necessary; ref_to_section_hash will load it if
    #needed
    $site->ref_to_section_hash or return;

    return $site->stash('homepage') || q{};
}

sub homepage_obj {

    my $site        = shift;
    my $homepage_id = $site->homepage_id;
    return $homepage_id unless $homepage_id;    #empty string or undef

    return $site->section_obj_by_id($homepage_id);
}

sub ref_to_section_hash {    #PRIVATE INTERNAL METHOD; COULD CHANGE
    my $site = shift;
    return $site->stash('section') if defined $site->stash('section');

    #haven't loaded the sections yet, do it now...
    $site->load_sections or return;
    return $site->stash('section');
}

sub ref_to_slug_hash {       #PRIVATE INTERNAL METHOD; COULD CHANGE
    my $site = shift;
    return $site->stash('slug') if defined $site->stash('slug');

    #haven't loaded the sections yet, do it now...
    $site->load_sections or return;
    return $site->stash('slug');
}

sub is_section_active {
    my ( $site, $section ) = @_;
    $section = $site->section_obj_by_id($section) if !ref $section;
    return if !$section || !$section->active;

    my @parents = $section->parents;
    shift @parents;    #homepage
    foreach my $pid (@parents) {
        my $parent = $site->section_obj_by_id($pid) or return;
        return if !$parent->active;
    }
    return 1;
}

sub all_descendants_ids {
    return _get_descendants(@_);
}

sub all_active_descendants_ids {
    my ( $site, $parent ) = @_;
    ( $parent ||= $site->homepage_obj ) or return ();
    my $parent_obj = ref $parent ? $parent : $site->section_obj_by_id($parent)
      or return ();   #either an error or no such section (would normally
                      #check for defined, but this method returns empty
                      #array on error, so okay just to pass back () either way
    return () if !$site->is_section_active($parent_obj);
    return _get_descendants( $_[0], $_[1], 'active_only' );
}

sub _get_descendants {
    my ( $site, $parent, $active_only ) = @_;
    ( $parent ||= $site->homepage_obj ) or return ();
    my $parent_obj = ref $parent ? $parent : $site->section_obj_by_id($parent)
      or return ();   #either an error or no such section (would normally
                      #check for defined, but this method returns empty
                      #array on error, so okay just to pass back () either way
    my @kids;
    return () if $active_only && !$parent_obj->active;
    foreach my $child_id ( $parent_obj->kids ) {
        defined( my $child_obj = $site->section_obj_by_id($child_id) )
          or return ();
        next if !$child_obj || ( $active_only && !$child_obj->active );
        push @kids,
          ( $child_id, $site->_get_descendants( $child_obj, $active_only ) );
    }
    return @kids;
}

sub users {
    my $site = shift;
    my $site_id   = $site->id;
    require BigMed::User;
    require BigMed::Priv;
    my $non_admin = BigMed::User->join_has(
        { join => 'BigMed::Priv', key => 'user' },
        { site => $site_id },
      )
      or return;

    #admins don't have priv objects, gather separately
    my $admin = BigMed::User->select( { level => 6 } )
      or return;
    my @all;
    foreach my $select ( $non_admin, $admin ) {
        my $obj;
        while ( $obj = $select->next ) {
            push @all, $obj;
        }
        return if !defined $obj;
    }
    return @all;
}


sub directory_url {
    return _directory_info( 'url', @_ );
}

sub directory_path {
    return _directory_info( 'dir', @_ );
}

sub _directory_info {
    my ( $type, $site, $section ) = @_;
    if ( $section && !ref $section ) {
        $section = $site->_get_section_obj($section) or return;
    }
    if (!$section || $section->is_homepage) {
        my $homepage = "homepage_$type";
        return $site->$homepage if !$section || $section->is_homepage;
    }
    my @parents = $section->parents;
    shift @parents;    #homepage
    my @slug_path;
    foreach my $sid (@parents) {
        my $sec = $site->_get_section_obj($sid) or return;
        push @slug_path, $sec->slug;
    }
    push @slug_path, $section->slug;
    my $html     = "html_$type";
    return join( q{/}, $site->$html, @slug_path );
}

sub _get_section_obj {
    my $site   = shift;
    my $sec_id = shift;
    defined( my $sec_obj = $site->section_obj_by_id($sec_id) ) or return;
    return $sec_obj
      || $site->set_error(
        head => 'SITE_Unknown section',
        text => ['SITE_TEXT_Unknown section', $sec_id]
      );
}


my $BM_HOMEFILES = qr/\Abm[~.] ( tags | (.*[.](shtml|xml|txt|js)) ) \z/msx;
my $BM_PAGEFILES =
  qr/\Abm[~.]
    (   styles|assets|pix|doc|tip|annc|pulldown
      | quick|latest|theme|comments|challenge|tagcloud
    )/msx;
my $INDEX_FILES = qr/\Aindex[~.](email|print)[.]shtml\z/;
sub trash_public_files {
    my $site = shift;

    my $homedir = $site->homepage_dir;
    if ($homedir && -e $homedir) {
        _delete_dir_regex($homedir, $BM_HOMEFILES) or return;
        _delete_dir_regex($homedir, $INDEX_FILES) or return;
        my $homepage = bm_file_path( $homedir, 'index.shtml' );
        bm_delete_file($homepage) or return;
    }

    my $pagedir = $site->html_dir;
    if ($pagedir && -e $pagedir) {
        _delete_dir_regex($pagedir, $BM_PAGEFILES) or return;

        #delete the directory of each main section
        defined( my $homesec = $site->homepage_obj ) or return;
        if ($homesec) {
            foreach my $mainsec ( $homesec->kids ) {
                defined( my $sec = $site->section_obj_by_id($mainsec) )
                  or return;
                next if !$sec || !defined $sec->slug || $sec->slug eq q{};
                bm_delete_dir( bm_file_path( $pagedir, $sec->slug ) )
                  or return;
            }
        }
    }
    return 1;
}

sub move_public_files {
    my ($site, $rdir) = @_;
    return 1 if !$rdir;
    my $new_homedir = $site->homepage_dir or return 1;
    my $new_htmldir = $site->html_dir or return 1;
    bm_confirm_dir($new_homedir, {build_path => 1}) or return;
    bm_confirm_dir($new_htmldir, {build_path => 1} ) or return;
    
    my $orig_homedir = $rdir->{orig_homepage_dir} || q{};
    if ($orig_homedir && $orig_homedir ne $new_homedir && -e $orig_homedir) {
        _move_dir_regex($orig_homedir, $new_homedir, $BM_HOMEFILES) or return;
        _move_dir_regex($orig_homedir, $new_homedir, $INDEX_FILES) or return;
        my $orig_homepage = bm_file_path( $orig_homedir, 'index.shtml' );
        if (-e $orig_homepage) {
            my $new_homepage = bm_file_path( $new_homedir, 'index.shtml' );
            bm_move_file($orig_homepage, $new_homepage) or return;
        }
    }
    
    my $orig_htmldir = $rdir->{orig_html_dir} || q{};
    if ($orig_htmldir && $orig_htmldir ne $new_htmldir && -e $orig_htmldir) {
        _move_dir_regex($orig_htmldir, $new_htmldir, $BM_PAGEFILES) or return;

        #move the directory of each main section
        defined( my $homesec = $site->homepage_obj ) or return;
        if ($homesec) {
            foreach my $mainsec ( $homesec->kids ) {
                defined( my $sec = $site->section_obj_by_id($mainsec) )
                  or return;
                next if !$sec || !defined $sec->slug || $sec->slug eq q{};
                
                my $slugdir = bm_file_path( $orig_htmldir, $sec->slug );
                next if !-e $slugdir;
                my $newdir = bm_file_path( $new_htmldir, $sec->slug );
                bm_move_dir( $slugdir, $newdir) or return;
            }
        }
    }
    return 1;
}

sub _delete_dir_regex { #delete files matching the regex from the dirpath
    my ($dirpath, $regex) = @_;
    my ( $DIR, $file );
    opendir( $DIR, $dirpath )
      or return BigMed::Error->set_io_error( $DIR, 'opendir', $dirpath,
        $OS_ERROR );
    while ( defined( $file = readdir $DIR ) ) {
        next if $file !~ $regex;
        my $path = bm_file_path( $dirpath, $file );
        if ( -d $path ) {
            bm_delete_dir($path) or return;
        }
        else {
            bm_delete_file($path) or return;
        }
    }
    closedir $DIR;
    return 1;
}

sub _move_dir_regex {
    my ($origdir, $newdir, $regex) = @_;
    my ( $DIR, $file );
    opendir( $DIR, $origdir )
      or return BigMed::Error->set_io_error( $DIR, 'opendir', $origdir,
        $OS_ERROR );
    while ( defined( $file = readdir $DIR ) ) {
        next if $file !~ $regex;
        my $opath = bm_file_path( $origdir, $file );
        my $npath = bm_file_path( $newdir, $file );
        if ( -d $opath ) {
            bm_move_dir($opath, $npath) or return;
        }
        else {
            bm_move_file($opath, $npath) or return;
        }
    }
    closedir $DIR;
    return 1;
}

sub _before_trash {
    my $site = shift;
    return 1 if !$site->id;
    $site->_trash_site_specific_files() or return;    
    _trash_systemwide_site_files([$site->id]) or return;  
    return 1;
}

sub _before_trash_all {
    my $selection = shift;
    return 1 if !$selection->count;
    
    my ($site, @id);
    $selection->set_index(0);
    while ( $site = $selection->next ) {
    $site->_trash_site_specific_files() or return;    
        $site->trash_public_files or return;
        push @id, $site->id;
    }
    return if !defined $site;
    return 1 if !@id;

    _trash_systemwide_site_files(\@id) or return;  
    return 1;
}

sub _trash_site_specific_files {
    my $site = shift;

    #html directory files
    $site->trash_public_files or return;

    #custom templates
    require BigMed::Template;
    my $tmpl = BigMed::Template->new($site);
    $tmpl->delete_all_custom_templates() or return;

    #search index
    my $sindex = bm_file_path( BigMed->bigmed->env('MOXIEDATA'),
        'search', 'site' . $site->id . '.cgi' );
    bm_delete_file($sindex) or return;

    #the data directory also gets removed, but that happens at the
    #driver level during the actual trash call
    return 1;
}

sub _trash_systemwide_site_files {
    my $rids = shift;
    return 1 if !@{$rids};

    require BigMed::Priv;
    my $privs = BigMed::Priv->select( { site => $rids } ) or return;
    $privs->trash_all or return;

    require BigMed::PageAlert;
    my $alerts = BigMed::PageAlert->select( { site => $rids } ) or return;
    $alerts->trash_all or return;

    require BigMed::JanitorNote;
    my $notes = BigMed::JanitorNote->select( { site => $rids } ) or return;
    $notes->trash_all or return;

    return 1;
}

sub clone_to_site {
    my ($site, $target) = @_;
    if (!$target || !ref $target || !$target->isa('BigMed::Site')) {
        croak 'clone_to_site requires target site object';
    }
    
    #has to be an empty site with unique id, directories and urls
    $target->update_id or return;
    croak 'target site cannot have the same id as the source site'
      if $target->id == $site->id;
    croak 'cannot clone to site with existing sections'
      if $target->homepage_obj;
      
    foreach my $col ( qw(name html_dir homepage_dir html_url homepage_url) ) {
        croak "target site must have a defined $col" if
          !defined $target->$col || $target->$col eq q{};
        croak "target site cannot have same $col as source site"
          if $target->$col eq $site->$col;
    }
    
    #callback allows plugins to register prefs etc
    $site->call_trigger('before_clone_to_site', $target);
    
    #clone sections and get the id mapping from old to new
    my $rmap = $site->_clone_section($target) or return;
    $site->_clone_prefs($target, $rmap) or return;
    
    #handle templates, theme files, css
    require BigMed::Theme;
    my $theme = BigMed::Theme->new();
    $theme->apply_site_theme($site, $target, $rmap) or return;
    
    return $rmap;
}

sub _clone_section {
    my ($site, $target, $osection, $rmap) = @_;
    $rmap ||= {};
    $osection ||= $site->homepage_obj or return $rmap;
    
    #get new section, set map
    my $nsection = $osection->copy({target_site => $target->id});
    $nsection->set_page(undef);
    my $secpage = $nsection->section_page_obj or return;
    $secpage->save or return;
    $rmap->{$osection->id} = $nsection->id;
    $target->add_section($nsection);    

    #update parent ids and clone the kids
    my @nparents = map { $rmap->{$_} || $_ } $osection->parents;
    $nsection->set_parents(\@nparents);
    $nsection->save or return; #do one save here in case probs below
    foreach my $kid_id ($osection->kids) {
        my $child = $site->section_obj_by_id($kid_id) or next;
        $site->_clone_section($target,$child,$rmap) or return;
    }
    my @nkids = grep {$_} map { $rmap->{$_} } $osection->kids;
    $nsection->set_kids(\@nkids);
    $nsection->save or return; #second save for kids
    
    return $rmap;
}

sub _clone_prefs {
    my ($site, $target, $rmap) = @_;
    
    $site->_import_prefs();

    my $all_prefs = BigMed::Prefs->select( { site => $site->id } );
    my $opref;
    while ( $opref = $all_prefs->next ) {
        next if !$opref->pref_exists($opref->name);  #defunct pref (eg plugin)
        my $npref = $opref->copy( { target_site => $target->id } );
        if ( $npref->section ) {
            my $nsec = $rmap->{ $npref->section } or next;
            $npref->set_section($nsec);
        }
        if ( BigMed::Prefs->pref_edit_type( $npref->name ) eq
            'select_section' )
        {
            my @value = $npref->pref_value();
            my @nsec;
            foreach my $sid ( @value ) {
                push @nsec, $rmap->{$sid} if $rmap->{$sid};
            }
            $npref->set_pref_value( \@nsec );
        }
        $npref->save or return;
    }
    return if !defined $opref;
    return 1;
}

sub _import_prefs {
    my ($site) = @_;

    #load formats to check for prefs with section ids as values
    require BigMed::Plugin;
    BigMed::Plugin->load_formats();
    
    #have to load non-format modules that register prefs, or
    #they won't be copied. add callback to allow plugins to announce
    #external modules.

    require BigMed::Comment;
    BigMed::Comment->register_comment_prefs();
    
    require BigMed::NoBots;
    BigMed::NoBots->register_antispam_prefs();
    
    require BigMed::Search;
    BigMed::Search->register_search_prefs();
    
    require BigMed::Media::Image;
    return;
}

1;
__END__

=head1 NAME

BigMed::Site - Big Medium site record

=head1 DESCRIPTION

A BigMed::Site object represents a website in the Big Medium system. It is used
to access and organize all of the settings, sections and widget preferences
for the site.

=head1 USAGE

BigMed::Site is a subclass of BigMed::Data. In addition to the methods 
documented below, please see the BigMed::Data documentation for details about
topics including:

=over 4

=item Creating a new data object

=item Saving a data object

=item Finding and sorting saved data objects

=item Data access methods

=item Error handling

=back

=head1 METHODS

=head2 Data Access Methods

The BigMed::Site object holds the following pieces of data. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation. See the L<"Searching and Sorting"> section below
for details on the data columns available to search and sort BigMed::Site
objects.

=over 4

=item * id

The numeric ID of the site

=item * name

The name of the site

=item * html_dir

The absolute path to the top directory where Big Medium stores the site's
HTML pages

=item * html_url

The URL for the top directory where Big Medium stores the site's HTML pages

=item * homepage_dir

The absolute path to the directory where Big Medium stores the site's homepage

=item * homepage_url

The URL for the directory where Big Medium stores the site's homepage

=item * flags

B<Hash reference.> A reference to the hash of site flags specified by the
various BigMed::Format subclasses. The key is the BigMed::Format flag name
and the value is true or false. See the BigMed::Format subclasses for details.

=item * site_doclimit

Numeric value (in kilobytes) of the maximum document size to allow Big Medium
users to upload to the site.

=item * time_offset

The time difference in hours from the server time. Big Medium uses this
difference to calculate the time displayed in the Big Medium control panel
and on the website.

=item * date_format

A string indicating the preferred date format for the site. See documentation
for BigMed's time_format method for details about the format of this string.

=item * time_format

A string indicating the preferred time format for the site. See documentation
for BigMed's time_format method for details about the format of this string.

=item * mod_time

The time when the site object was last saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=item * create_time

The time when the site object was first saved to disk, in UTC (Greenwich Mean)
time. The format: YYYY-MM-DD HH:MM:SS

=back

=head2 Additional accessors

These are getter methods to access the location of the site's image and
document directories. (Unlike the data columns listed above, these
attributes do not have corresponding setter methods.)

=over 4

=item * image_url

URL of the site's image directory.

=item * image_path

Path to the site's image directory.

=item * doc_url

URL of the site's document directory.

=item * doc_path

Path to the site's document directory.

=back

=head3 Searching and Sorting

You can look up and sort records by any combination of the following fields.
See the C<fetch> and C<select> documentation in BigMed::Data for more info.

=over 4

=item * id

=item * mod_time

=item * name

=item * html_dir

=item * homepage_dir

=item * html_url

=item * homepage_url

=back

=head3 C<trash> and C<trash_all> methods

When the C<trash> and C<trash_all> data methods are called on BigMed::Site
objects, a few additional steps are taken in addition to deleting the object
from the data store:

=over 4

=item * Big Medium files and directories are removed from the public site.
The homepage directory and the page directory are not removed, but many
files within them are deleted:

=over 4

=item * index.shtml, index.[...].shtml and index~[...].shtml are removed
from homepage directory

=item * All files and directories beginning with bm~ or bm. are removed from
the homepage and page directories.

=item * The section directories of all main sections and <B>ALL</b> contents
are removed.

=item * All site-specific data (content, sections, media, templates, etc)
are removed.

=item * All user privilege objects for the site are removed.

=item * All BigMed::PageAlert objects for the site are removed.

=back

=head3 C<copy> and C<clone_to_site> methods

BigMed::Site's C<copy> method works the same as described in BigMed::Data
with no extra bells or whistles. If you want to create a copy of a site
and its associated data objects, use C<clone_to_site>.

=head4 C<<$site->clone_to_site($target_site)>>

Applies the section structure, preferences, templates and theme of the
site to the target site object in the argument. The target site should
not yet have any sections, not even a homepage. It should be a fresh
site object with its basic data populated:

=over 4

=item * name

=item * html_dir

=item * homepage_dir

=item * html_url

=item * homepage_url

=back

If successful, the method returns a hash reference with a mapping of the
original site's section IDs to those of the cloned site. Keys are the ids
for the original sections and values are the corresponding sections in
the new site.

The method returns false if there's an error (and adds a message to the
BigMed::Error queue).

Note that this method does not copy content objects or user privileges. It
effectively creates an empty shell site that matches the structure and
appearance of the original site. The following objects and items are cloned
to the target site:

=over 4

=item * BigMed::Section objects

=item * BigMed::Prefs objects

=item * BigMed::CSS objects

=item * All templates

=item * Theme CSS

=item * Theme assets

=back

BigMed::Site has a C<before_clone_to_site> callback trigger event which
allows plugins, for example, to register preferences that should be
copied when a site is cloned (only registered preferences are cloned
to the target site). The callback receives the source site and the target
(clone) site as arguments:

    $site->add_callback('before_clone_to_site', \&my_callback);
    
    sub my_callback {
        my ($site, $target_site) = @_;
        ...
        return 1;
    }

For more information on trigger callbacks, see BigMed::Data.

=head3 Directory Management

=head4 C<<$site->move_public_files( \%dir_info )>>

Moves Big Medium's public files in the old directories specified
in the parameter argument to the homepage_dir and html_dir paths
in the site object. Returns true on success.

    $site->move_public_files( {
        orig_homepage_dir = '/path/to/old/homepagedir',
        orig_html_dir => '/path/to/old/htmldir',
    } ) or $site->error_stop;

You can specify either or both of the orig_homepage_dir and orig_html_dir
parameters. No files are moved if neither directory is specified or
if neither directory is different from their current counterparts
in the site object.

=head4 C<<$site->trash_public_files>>

This method remove all of the public-site files and directories described
above. It is used internally when the <trash> and <trash_all> methods are
called. It returns true on success or false on error (in addition to adding
a message to the Big Medium error queue).

=head2 Section Management Methods

The BigMed::Site object organizes and caches the site's section objects, using
the following methods.

=over 4

=item $site->load_sections

Loads and caches the site's section objects into memory.  Returns some true
value if successful. On error, returns undef (see the error queue for the error
message).

    $site->load_objects or $site->error_stop;

Calling load_objects subsequent times will wipe out the previous cache and
reload the section info fresh from disk (any unsaved sections will no longer
be in the cache or available via C<section_obj_by_id> or
C<section_obj_by_slug>).

Note: All of the section management methods listed below will call load_objects
to create the cache if sections have not yet been loaded.

=item $site->section_obj_by_id($section_id);

Returns the section object specifed by the section id. An empty-string return
value indicates no such section exists in site; undef indicates that there was
an error retrieving the section (check the error queue for the error message).

    defined ( $section = $site->section_obj_by_id($section_id) )
      or $site->error_stop; #error loading from disk
    $section or print "No section with id $section_id;

=item $site->section_obj_by_slug($slug_name);

Returns the section object specified by the slug name. An empty-string return
value indicates no such section exists in site; undef indicates that there was
an error retrieving the section (check the error queue for the error message).

    defined ( $section = $site->section_obj_by_slug($slug_name) )
      or $site->error_stop; #error loading from disk
    $section or print "No section named $slug_name";

=item $site->homepage_id;

Returns the id of the homepage section. Returns empty string '' if no
homepage is found. Returns undef if error.

    defined ( $homepage_id = $site->homepage_id )
      or $site->error_stop; #hit an error retrieving section info
    $homepage_id or print "No homepage has been declared";

=item $site->homepage_obj;

Returns the site's homepage section object. Effectively like:

    $homepage = $site->section_obj_by_id( $site->homepage_id );

Returns empty string '' if no homepage has been declared. Returns undef
if error.

    defined ( $homepage = $site->homepage_obj )
      or $site->error_stop; #hit an error retrieving section
    $homepage or print "No homepage has been declared";

=item $site->all_descendants_ids($section_obj_or_id)

Returns an array of IDs of all of descendant sections of the section specified
in the argument. If no section is specified, the homepage section will
be used.

The order of the IDs is delivered in tree order so that a section and all of
its descendants appear in the array before the section's siblings. So, if
you have a section tree that looks like this:

    Section 1
    -- Subsection 1A
    -- Subsection 1B
    -- -- Sub-subsection 1Ba
    -- -- Sub-subsection 1Bb
    Section 2
    Section 3
    -- Subsection 3A
    -- Subsection 3B

...then the returned array would return the corresponding section IDs in this
order:

    Section 1
    Subsection 1A
    Subsection 1B
    Sub-subsection 1Ba
    Sub-subsection 1Bb
    Section 2
    Section 3
    Subsection 3A
    Subsection 3B

This has the happy effect of allowing you to easily display the sections in
tree order. For example:

    foreach my $section_id ( $site->all_descendants_ids() ) {
        my $section_obj = $site->section_obj_by_id($section_id)
          or $site->error_stop;
        print "-" for ( 1..scalar($section_obj->parents) );
        print " ", $section_obj->name, "\n"
    }

=item $site->all_active_descendants_ids($section_obj_or_id)

Same as C<all_descendants_ids> but includes IDs of sections only if they
(and all their parents) have a true value in their "active" columns.

=item $site->is_section_active($section_obj_or_id)

Returns true if the section and all of its parents have a true value
in their "active" columns.

=item $site->add_section($section_obj);

Adds a section to the site by making the following updates to the section
object:

=over 4

=item * set the section id if it needs one

=item * set the section's site attribute to the site id

=item * add the object to the site object's in-memory section cache

=item * add the section's slug name and id to the internal maps used by
C<section_obj_by_id> and C<section_obj_by_slug>

=back

Accepts a section object as an optional argument (if no section object
is provided, a new section object is created). The section object is the
return value, or undef if there is a problem getting a unique id.

    #add an existing section object
    defined ( $section_obj = $site->add_section($section_obj) )
      or $site->error_stop;
    
    #create and add a new section object
    defined ( $new_section = $site->add_section() )
      or $site->error_stop;

Note: The method does not save the section to disk. Also, any subsequent
changes to the section's id or slug name are not automatically updated
in the site's C<section_obj_by_id> and C<section_obj_by_slug> in-memory
maps. To register a change in the section's id or slug name, submit the
section to the C<add_section> method again, or use C<load_objects>
to refresh all data from disk.

=item * $site->directory_url($section);

Returns the URL to the HTML directory for the section in the argument.
If no section is specified, the homepage directory URL is returned.
Returns undef (and sets a message in the error queue) if there is an error.

=item * $site->directory_path($section);

Returns the path to the HTML directory for the section in the argument.
If no section is specified, the homepage directory path is returned.
Returns undef (and sets a message in the error queue) if there is an error.

=back

=head2 Preference Management Methods

The BigMed::Site object is the traffic cop for a site's widget preferences, using
the following methods.

=over 4

=item $site->get_pref_value($pref_name, $section_obj_or_id)

Returns the effective value for the selected preference and section.
An "effective" preference value is the value that applies to the
section, even if that section does not itself have a custom value specified
for the section.

If there is an error retrieving the value, the method adds an error message
to the error queue and returns undef in a scalar context, or an empty set
in array and hash contexts. To catch errors, these formats are recommended
(replace $site->error_stop with whatever you want to do when an error is
reported):

    #get scalar
    defined ( $value = $site->get_pref_value($pref_name, $section) )
      or $site->error_stop;
    
    #get array
    @value = $site->get_pref_value($pref_name, $section)
      or ($site->error && $site->error_stop);
    
    #get hash
    %value = $site->get_pref_value($pref_name, $section)
      or ($site->error && $site->error_stop);

The method accepts these arguments:

=over 4

=item * preference name

[Required] The name of the widget preference to save. The preference name must
be a registered BigMed::Format preference (see the BigMed::Format
subclasses for available widget preferences).

=item * section object or id

[Optional] The section object or id of the section whose preference value you
want. If the section id is omitted, the method returns the site's default
value.

=back

B<Inheritance.>
Sections inherit the preference values from their parent sections. For
example, if a subsection does not have its own custom preference value,
the method returns the preference value for the parent section (or, if
the parent has no custom preference, for the parent's parent, etc.).

B<Homepage and inheritance.>
In a content context, the homepage is considered to be the parent section of
all top-level sections. However, this is not the case for widget preferences,
since the homepage often has distinctly different rules for widget
display than other sections. Instead, the parent for top-level
sections (and for the homepage, too) is the site itself. When the homepage
or a top-level section does not have its own custom preference value,
the method returns the site's default preference.

B<Fallback preferences.> Some widget preferences specify "fallback"
preferences whose value should be used if no preference value is specified
even at the sitewide level. In those cases, the method returns the value
for that fallback preference.

=item $site->store_pref($pref_name, $value, $section_id_or_obj)

Saves a widget preference value to disk. Returns some true value if successful,
or undef on error.

    $site->store_pref('pref_name', $value, $section_id_or_obj)
      or $site->error_stop;

The method accepts these arguments:

=over 4

=item * preference name

[Required] The name of the widget preference to save. The preference name must
be a registered BigMed::Format preference (see the BigMed::Format
subclasses for available widget preferences).

=item * value

[Required] The value to save. Array and hash values should be submitted as
references.

=item * section id or object

[Optional] The section id or section object for the section to which you would
like to apply this preference value. If not supplied, the preference will be
saved as the default preference for the entire site; the value will be used by
sections that do not have a custom preference.

=back

=item $site->clear_pref($pref_name, $section_id)

Removes the preference value for the specified section. The method returns some
true value if successful, undef if there was an error. On error, an error
message is added to the error queue.

    $site->clear_pref($pref_name, $section_id)
      or $site->error_stop;

The method accepts these arguments:

=over 4

=item * preference name

[Required] The name of the widget preference to clear. The preference name must
be a registered BigMed::Format preference (see the BigMed::Format
subclasses for available widget preferences).

=item * section id

[Optional] The id of the section whose preference you want to clear. If no
section id is specified, the sitewide default value for the preference will
be cleared.

=back

=item $site->pref_inherits_from_fallback($pref_name, $section_obj_or_id)

Returns true if the preference does not have its own value or default value
and is inheriting from its fallback widget.

=back

=head2 User Info

=head3 C<<$site->users()>>

Returns an array of user objects for all users with privileges at the site.
The array is not sorted in any particular order.

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=item * BigMed::Section

=back

=head1 AUTHOR & COPYRIGHTS

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

