# 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: MiniContent.pm 3113 2008-06-16 16:23:25Z josh $

package BigMed::MiniContent;
use strict;
use utf8;
use Carp;

use base qw(BigMed::Data);


my %minicontent;
BigMed::MiniContent->set_schema(
    source => 'minicontent',
    elements => [
        {   name  => 'site',
            type  => 'system_id',
            index => 1,
        },
    ],
);

###########################################################
# REGISTRATION
###########################################################

sub register_minicontent {
    my $class = shift;
    my %arg   = @_;
    $class eq "BigMed::MiniContent"
      && croak "register_content_class must be invoked via a BigMed::Content "
      . "subclass";

    #register as a data object
    $class->set_schema(
        source   => $arg{source},
        label    => $arg{label},
        elements => $arg{elements},
    );

    my %metadata;
    foreach my $key (
        'editor_fields', 'editor_abbr',
        'preview',       'html_generator',
        'can_embed',      'sort_fields',
        'can_link', 'can_hotlink', 'post_parse',
        'alpha_sort_col', 'pre_preview',
      )
    {
        $metadata{$key} = $arg{$key} if exists $arg{$key};
    }

    $minicontent{$class} = \%metadata;

    1;
}

sub _effective_value {
    my $class     = shift;
    my $attribute = shift;
    return undef if $class eq 'BigMed::MiniContent';
    if ( !exists $minicontent{$class}->{$attribute} ) {    #inherit
        no strict 'refs';
        my $parent = ${ $class . "::ISA" }[0];
        return $parent->_effective_value($attribute);
    }
    return  $minicontent{$class}->{$attribute};
}

BigMed::MiniContent->add_callback( 'before_trash', \&_trash_pointers );
BigMed::MiniContent->add_callback( 'before_trash_all', \&_trash_all_pointers );

sub _trash_pointers {
    my $obj = shift;
    return 1 if !$obj->id || !$obj->site;
    my $defunct_pointers = BigMed::Pointer->select(
        {   site         => $obj->site,
            target_table => $obj->data_source,
            target_id    => $obj->id,
        }
      )
      or return;
    return $defunct_pointers->trash_all;
}

sub _trash_all_pointers {
    my $select = shift;
    my ($obj, $sid, $class, @id);
    while ($obj = $select->next) {
        $class ||= ref $obj;
        $sid ||= $obj->site;
        push @id, $obj->id;
    }
    return 1 if !@id || !$class || !$sid;
    my $defunct_pointers = BigMed::Pointer->select(
        {   site         => $sid,
            target_table => $class->data_source,
            target_id    => \@id
        }
      )
      or return;
    return $defunct_pointers->trash_all;
}


sub save_submission {
    my $self = shift;
    my %param = @_;
    my ($abbr_fields, $suffix, $prefix, $user, $parser, $use_fields) =
      @param{ qw(abbr_fields field_suffix field_prefix user parser use_fields) };
    $suffix ||= q{};
    $prefix ||= q{};
    my $data_class = ref $self;
    
    if ($user && !$self->editable_by_user($user) ) {
        return $self->set_error(
            head => 'CONTENT_Not Allowed To Do That',
            text =>
              'CONTENT_TEXT_You do not have permission to edit that content',
        );
    }
    
    my @fields;
    my %id_map = ( '_ERROR' => '_ERROR' );
    my @editor_fields =
        $use_fields  ? @{$use_fields}
      : $abbr_fields ? $self->editor_abbr()
      : ();
    @editor_fields = $self->editor_fields if !@editor_fields;
    foreach my $rf ( @editor_fields ) {
        my %info = %{$rf};
        my $dep  = $info{depend};
        next if $dep && $self->can($dep) && !$self->$dep;
        my $id = $info{'_orig'} = $info{id} || $info{column} || q{};

        $id_map{ $info{id} = "$prefix$id$suffix" } = $id;
        $info{data_class} ||= $data_class;
        push @fields, \%info;
    }
    
    my $app = BigMed->bigmed->app;
    $parser ||= sub { $app->parse_submission(@_) };
    my %results;
    {    #parse with full prefix/suffix id changes, and convert to true id
        my %full_id_results = $parser->(@fields);
        %results =
          map { ( $id_map{$_} || $_ ) => $full_id_results{$_} }
          keys %full_id_results;
    }
    $self->post_parse( \%results );

    #error handling
    if ( $results{_ERROR} ) { #revert to actual IDs and set error; return
        my %rev_error =
          map { ( ( $id_map{$_} || $_ ) => $results{_ERROR}->{$_} ) }
          keys %{ $results{_ERROR} };
        
        foreach my $rfield ( @fields ) {
            my %info = %{$rfield};
            my $id = $info{'_orig'} || q{};
            next if !$rev_error{$id};
            my $ref =
              $app->prompt_field_ref( %info, data_class => $data_class );
            my $label = $ref->[2]->{label} || $id;
            my $msg = $app->language( $rev_error{$id} );
            $self->set_error(
                head => 'BM_Trouble_processing_form',
                text => ['CONTENT_parse_error', $label, $msg]
            );
        }
        return;
    }

    #stow the values and do uniqueness checks
    my %properties = $self->properties;
    my $has_parse_error;
    foreach my $rfield (@editor_fields) {    #stuff values into object
        my $id = $rfield->{column} || $rfield->{id};
        my $method = "set_$id";
        next if !$self->can($method);

        $self->$method( $results{$id} );
        if (   defined $results{$id}
            && $results{$id} ne q{}
            && $properties{$id}->{unique}
            && !$self->is_unique($id) )
        {
            my $ref =
              $app->prompt_field_ref( %{$rfield},
                data_class => $data_class )->[2];
            my $label = $ref->{label} || $id;
            my $type = $app->language($self->data_label);
            $has_parse_error = 1;
            $app->set_error(
                head => 'BM_HEAD_Not unique',
                text => ['BM_Not unique', $label, $type]
            );
        }
    }
    return if $has_parse_error;
    
    if ( $self->can('set_owner') && $user && !$self->owner ) {
        $self->set_owner( $user->id );
    }
    return $self->save();
}

###########################################################
# METADATA ACCESSORS
# all metadata is inheritable from parent classes
###########################################################


sub pre_preview {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('pre_preview');
}

sub alpha_sort_col {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('alpha_sort_col');
}

sub can_hotlink {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('can_hotlink');
}

sub can_link {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('can_link');
}

sub editor_fields {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    my $rarray = $class->_effective_value('editor_fields');
    return $rarray ? @$rarray : ();
}

sub editor_abbr {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    my $rarray = $class->_effective_value('editor_abbr')
      || $class->_effective_value('editor_fields');
    return $rarray ? @$rarray : ();
}

sub preview {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    my $rhash = $class->_effective_value('preview');
    return $rhash ? %$rhash : ();
}

sub html_generator {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('html_generator');
}

sub can_embed {
    my $self = shift;
    my $class = ref $self || $self;
    croak "$class is not registered as minicontent" if !$minicontent{$class};
    $class->_effective_value('can_embed');
}

sub editable_by_user {
    my $obj  = shift;
    my $user = shift or return 1;    #allowed if no user specified
    my $site = $obj->site;

    #webmasters and admins can edit; otherwise, must be owner w/site privs
    my $priv_level = $user->privilege_level($site);
    return 1 if $priv_level > 4;    #webmaster or admin
    my $is_owner = !$obj->can('owner')                #no such restriction
      || !defined $obj->owner                         #no owner named
      || $obj->owner == $user->id;                    #user is the owner
    return $is_owner && $priv_level;
}

sub post_parse {
    my ($self, $rfields) = @_;
    my $class = ref $self;
    my $coderef = $class->_effective_value('post_parse');
    return $coderef ? $coderef->($self, $rfields) : 1;
}

sub text_position_lang {
    my $obj = shift;
    my $position = shift || 'block:1';
    my $pos_text;
    if ( index( $position, 'block' ) == 0 ) {
        $pos_text = ['MINICONTENT_at paragraph', substr( $position, 6 )];
    }
    else {
        $pos_text = "MINICONTENT_position $position";
    }
    $pos_text;
}

sub text_align_lang {
    my $obj = shift;
    my $align = shift || 'default';
    return "MINICONTENT_align $align";
}

my %pos_order = (
    above  => 0,
    below  => 888888888,
    hidden => 999999999,
);
sub sort_embedded {
    my $self = shift;
    return @_ if !$self->can_embed;
    map { $_->[0] }
      sort { $a->[1] <=> $b->[1] || $b->[2] <=> $a->[2] }  #position, priority
      map {
        my ( $type, $n ) = split( /:/, ( $_->position || 'block:1' ) );
        my $priority = $_->can('priority') ? ( $_->priority || '0' ) : '500';
        [$_, ( $n || $pos_order{$type} || 0 ), $priority];
      } @_;
}

sub exists_at_site { #stub method; override at subclass
    my ($obj, $site, $rparam) = @_;
    return 0;
}

sub sanitize_preview_html {
    my ( $self, $app, $text ) = @_;
    $text = q{} if !defined $text;
    if ( $text =~ /<(form|input|textarea|button|select|script)[^>]*>/ms ) {
        $text =
            '<p class="bmcpSupportText">'
          . $app->language('MINICONTENT_Preview text contains form')
          . '</p>';
    }
    return $text;
}

1;

__END__

=head1 NAME

BigMed::MiniContent - Base class for Big Medium mini-content records,
which may be mixed into BigMed::Content objects.

=head1 DESCRIPTION

Mini-content elements are secondary content items like images, documents,
events, tags, pullquotes, etc that are related to primary content
(BigMed::Content) elements like pages, announcements, tips, etc.
These are the target objects for the relationships defined in
BigMed::Content.

BigMed::MiniContent provides methods for registering these mini-content
classes and retrieving editor fields and other metadata for the classes.

=head1 ABOUT MINI-CONTENT CLASS DEFINITIONS

Registration of a mini-content class establishes the class as a
database-backed storage class and also creates an in-memory definition
of the content type, including:

=over 4

=item * Database source and schema for the class's content objects

=item * Data field information for providing an editing interface

=item * Callback routine for displaying previews of the mini-content.

=back

=head2 Data schema

BigMed::MiniContent subclasses are also subclasses of BigMed::Data which
allows them to save and retrieve objects from disk. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation.

Every subclass of the BigMed::MiniContent class inherits the following data
columns, which may be further extended at registration. All of these fields
can be used for sorting and searching via the C<fetch> and C<select>
methods of BigMed::Data subclasses.

=over 4

=item * id

The content object's unique ID

=item * site

ID of the site (BigMed::Site) to which the content belongs

=item * mod_time

Timestamp for when the last time the object was saved.

=item * create_time

Timestamp for when the object was first created.

=back

=head1 Registration of BigMed::MiniContent Subclasses

=head2 C<< Foo->register_minicontent(%arguments); >>

Registers a mini-content class definition for class Foo in the system and
establishes the data structure and database table to be used for
persistence. Note that Foo must be a subclass of BigMed::MiniContent (or
one of its subclasses):

    package Foo;
    use base qw(BigMed::MiniContent);
    Foo->register_minicontent(%arguments);

Registering a class that has already been registered overwrites the previous
class definition.

The hash of arguments consists of the following key-value pairs. (If any
attributes are left undefined, the registered class will inherit defined
values from parent mini-content classes, if any.)

=over 4

=item * source => $database_source

The name of the data source. This should be a name that is unique
to this subclass and will be used internally by the data driver,
which could use it as a SQL table name, a file directory, etc. It
is recommended to keep this name simple: a lowercase name that
consists only of the letters a-z.

If left undefined, the source name will correspond to the lower-case, plural
version of the last portion of the class name.

=item * label => "public name"

The name to use as an internal identifier (in localization text, for example)
to refer to this subclass within Big Medium.  If left undefined, the source
name will correspond to the lower-case version of the last portion
of the class name -- same treatment as source, above, but not pluralized.

=item * elements => \@data_schema

A reference to the data schema array for the content class. This data schema
is added to the data schema inherited from the parent class. For details on
the composition of the data schema array, see the BigMed::Data documentation.

=item * editor_fields => \@editor_fields

Defines the recommended order and content of the fields to present
to the user for editing in a Big Medium application's content editor context
(for example, by BigMed::App::Web::Editor).

The editor fields defined here are used when editing an object within its
own dedicated editing interface.
Unless the C<editor_abbr> attribute is also
supplied (see below), these editor fields are used as well when editing
within the editor interface of a primary content object (when adding images
while editing a BigMed::Page object for example).

The value of the editor_fields attribute is a reference to an array of hash
references, suitable to be passed to BigMed::App's prompt_field_ref method:

    editor_fields => [
        { column => 'text', },
        { column => 'alignment', },
        { column => 'owner', },
      ]

Note that you can omit the data_class key/value pair in each hash reference
and the system will assume the data class of the object itself.

=item * editor_abbr => \@editor_fields

Defines an optional "abbreviated" set of editor fields to be used when
editing the object as a related object within a primary content object's
editing interface. If left undefined, the editor_fields array will be
used instead.

The value follows the same format as editor_fields.

=item * preview => { html => \&build_preview_html }

A hash reference containing various key/value pairs for coderefs that
generate content previews in various formats (most commonly, html).
The preview is used in application's content editors.

The callback routine receives three arguments:

=over 4

=item 1 The application object

=item 2 The mini-content object to preview (or, if it's a 'points_to'
relationship, an array reference where the first item is a BigMed::Pointer
object, and the second item is the mini-content object)

=item 3 A hash reference of routine-specific options.

=back

The return value should vary according to the format being returned.
The commonly used html format expects a hash with two key/value pairs,
the parameters that get used in the wi_minicontent_item.tmpl html template:

=item * pre_preview => \&pre_preview_routine

An optional coderef to call before sending previewed objects to the preview
routine for display in the browser and before processing into json. The
routine receives two arguments: the current BigMed::App object and an
array ref to the list of objects to be previewed. (For objects in points_to
relationshpis, this is actually an array of array references where the
first element is the BigMed::Pointer object and the second is the object
itself).

=item * post_parse => \&post_parse_routine

An optional coderef to call after a mini-content object's editor_fields fields
have been submitted and parsed but before they are applied to the minicontent
object. The routine receives two arguments: the original BigMed::MiniContent
object before the fields are applied, and a hash reference to the BigMed::App
parse_submission results.

After the callback returns, the parse_submission hash is checked for
errors and, if clear, is used to fill the mini-content object.

=over 4

=item * PREVIEW_HTML => $main_html

The html to display the preview content.

=item * STATUS_HTML => $status_html

The html to display in the status line of the preview panel (commonly,
metadata information).

=back

It comes together like so:

    my $preview = Foo->preview();
    my $callback = $preview{html};
    my %template_param = $callback->($app, $object);

=item * html_generator => \&html_generator

A code reference to a callback routine that generates the html to be
published for the content item. 

The callback routine receives two arguments:

=over 4

=item 1 The application object

=item 2 The mini-content object to preview (or, if it's a 'points_to'
relationship, an array reference where the first item is a BigMed::Pointer
object, and the second item is the mini-content object)

=back

The routine should return a string of html:

    my $generate_html = Foo->html_generator();
    my $html = $html_generator->($app, $object, \%param);

=item * alpha_sort_col => $col_name | \@cols

An optional field indicating the column(s) to use to present mini-content
objects in alphabetical order. May be a string with a column name or
an array reference to multiple columns:

    #single column
    alpha_sort_col => 'title'
    
    #multi-column
    alpha_sort_col => ['last_name', 'first_name']

If no sort column is specified, a default value of 'title' will be tried
or 'name' if 'title' is not available.

=item * can_embed => 1

If true, the mini-content object may be embedded in article text for display,
and the widget builders will incorporate these objects into article text.
The data schema for embedded objects (or the relationship data schema
for points_to relationship objects) should include align and position
columns to handle horizontal alignment and text position respectively.
(See Common Data Colums below.)

=item * can_link => 1

If true, the mini-content object may be included in links to article pages
and appopriate widget builders may incorporate them into link text.
The data schema for can_link objects (or the relationship data schema
for points_to relationship objects) should include link_position
columns to handle specifying which links, if any, the object should
be included with. (See Common Data Columns below.)

=item * can_hotlink => 1

If true, the mini-content object may be linked to an external URL
in certain contexts. The data schema for can_hotlink objects (or the
relationship data schema for points_to relationship objects) should include a
hotlink_url columns to handle specifying the URL, if any, to which the
object should link. (See Common Data Columns below).

=item * sort_fields => \@column_names

An array reference to the columns to sort by in ascending order.

=item * has_browser => 1

If true, application editors should allow users to browse existing articles
from a library of items when editing.

=back

=head2 Common Data Columns

There are several common data columns that BigMed applications may look for
to achieve certain goals. Include these columns in your subclass data
schema as appropriate, or in the relationship metadata for points_to
relationship objects (these columns are not included by default; they're
just expected by Big Medium in certain contexts):

=over 4

=item * owner

The ID of the user who owns the item. If present, Big Medium apps should
allow only that user or users with webmaster or admin privileges to edit
the object. The data schema definition:

    {
        name     => 'owner',
        type     => 'system_id',
        index    => 1,
    }

=item * shared

A boolean value indicating whether the item may be used by other users at
the same site. The data schema definition:

    {
        name     => 'shared',
        type     => 'boolean',
        default  => 1,
        index    => 1,
    }

=item * position

For objects that may be embedded within article text, this value indicates
where within the text it should be located. The data schema definition:

    {
        name  => 'position',
        type  => 'body_position',
        default => 'block:1',
    }

=item * align

For objects that may be embedded within article text, this value indicates
the horizontal alignment of the object relative to the text.
The data schema definition:

    {
        name  => 'align',
        type  => 'value_list',
        options => ['default', 'left', 'center', 'right'],
        default => 'default',
    }

=item * link_position

For objects that may be included as part of links to article pages, this
value indicates which link types, if any, the object may be associated
with (specific link widgets and widget prefs determine whether and how
the object should actually be included). The data schema definition:

    {
        name  => 'link_position',
        type  => 'value_list',
        options => ['none', 'all', 'spotlight', 'links'],
        default => 'none',
    }

=item * hotlink_url

For can_hotlink objects, this value is the URL for the hotlink.

    {
        name  => 'hotlink_url',
        type  => 'url',
    }

=back

=head1 Accessor Methods

=head2 Foo->editor_fields

Returns an array of fieldsets containing the fields that are editable for this
mini-content class, the de-referenced value of the editor_fields attribute
registered via C<register_minicontent>.

    #static class method or object method
    @editor_fields = Foo->editor_fields
    @editor_fields = $foo->editor_fields

=head2 Foo->editor_abbr

Returns an array of fieldsets containing the fields to be edited in the
context of a primary content object's editing interface. This is the
de-referenced value of the editor_abbr attribute registered via
C<register_minicontent>.

    #static class method or object method
    @editor_fields = Foo->editor_abbr
    @editor_fields = $foo->editor_abbr

=head2 $object->post_parse(\%parsed_fields)

Submits the object and parsed editor_fields to the class's post_parse
callback, if any. Returns undef if an error is encountered.

=head2 Foo->preview

Returns a hash of the preview callbacks. This is the de-referenced value of
the preview attribute registered via C<register_minicontent>.

    #static class method or object method
    %preview = Foo->preview
    %preview = $foo->preview

    my $html_callback = $preview{html};
    
=head2 Foo->html_generator

Returns the code reference registered in the html_generator attribute via
C<register_minicontent>.

    #static class method or object method
    $callback = Foo->html_generator
    $callback = $foo->html_generator

=head2 Foo->can_embed

Returns true if Big Medium applications should allow the class objects to
be embedded for display within the text of primary content objects.

    #static class method or object method
    $can_embed = Foo->can_embed
    $can_embed = $foo->can_embed

=item * $obj->editable_by_user($user)

Returns true if the minicontent object is editable by the user (always returns
true if no user is specified).

The object is considered editable if the user has admin or webmaster privileges
at the site. If the object has an owner column, the user may also edit the
content if the owner column matches the user id, or if the owner column
is undefined.  If the object has no owner column, then no restrictions are
applied.

=item * $obj->text_position_lang($obj->position)

Returns a BigMed::Language lexicon key for text position. Useful for objects
with a position column.

=item * $obj->text_align_lang($obj->align)

Returns a BigMed::Language lexicon key for text aligment. Useful for objects
with an align column.

=back

=head1 Utility Methods

=head2 C<< $obj->save_submission( %args ) >>

Saves user-submitted fields to the object. Returns true on success and false if there's an error (including a user error like a missing field etc). Error messages are placed in the error queue.

The method accepts the following arguments in the argument hash:

=over 4

=item * user => 1

Optional BigMed::User object to indicate which user is saving the file. If provided, the method checks that the user has permission to edit the object (otherwise returns an error) and sets the user as the owner if the object does not yet have an owner (provided that the object has a set_owner method).

=item * use_fields => \@fields

If provided, the arrayref is used as the field definition to use to read;
if not provided, the C<editor_fields> list is used (unless abbr_fields is
set to true, see below).

=item * abbr_fields => 1

If true, uses the C<editor_abbr> fields instead of the default behavior of using the full C<editor_fields> list.

=item * field_prefix => 'prefix_'

Option string indicating a prefix that has been added to the beginning of field IDs in the query string so that the parser can find the correct field names.

=item * field_suffix => '_suffix'

Option string indicating a suffix that has been added to the end of field IDs in the query string so that the parser can find the correct field names.

=item * parser => \&coderef

By default, the method uses the current application's C<parse_submisison> method to parse and scrub all submissions. However, you can provide your own coderef to handle this instead. The routine receives the editor_fields array of hash references as its arguments.

=back

=head2 Foo->sort_embedded( @objects )

For MiniContent classes that can embed in article text, sorts objects by
their position and (if applicable) priority columns, which is useful for
display in editing contexts.

Returns an array of the objects, sorted in order that they will be inserted
into the page text.

May be called as either a class or object method:

    @sorted = BigMed::Pullquote->sort_embedded(@pullquotes);    #class method
    @sorted = $pullquote->sort_embedded(@pullquotes);           #object method

Note that this is useful primarily for "has" relationships with objects whose
data schema include the position and (optionally) priority columns in the
object itself.

=head2 Foo->sanitize_preview_html( $app, $html )

Some browsers, notably Internet Explorer, can cause Big Medium's edit form
to break if a MiniContent object's preview HTML contains a form. MiniContent
classes can call this method which detects if the html contains troublesome
tags (form, input, textarea, button, select or script) and, if so, returns
some alternate "text not available for preview" HTML which may be used
instead of the original HTML.

=head1 Checking for Duplicates

BigMed::MiniContent provides a stub method named C<exists_at_site> which should
be overridden by each subclass. It is intended to be used to detect
whether an object already exists at the site associated in the argument
site object. This is basically of use only for objects that are part
of a "points_to" relationship and is unnecessary for "has" object
relationships.

    my $test = $mini_object->exists_at_site($site_object);
    if ($test) {
        print 'object already exists at site!';
    }

If the object already exists, the method returns the object from that site.
If not, it returns 0 (or undef on error).

The implementation is subclass-specific. See documentation in each subclass
for details. (The BigMed::MiniContent stub method always returns 0).

=head1 SEE ALSO

=over 4

=item * BigMed::Data

=item * BigMed::Content

=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

