<?php
/* $Id: acidfree.module,v 1.62.2.18 2006/04/25 13:40:19 vhmauery Exp $ */

/*
Acidfree Photo Albums for Drupal
Copyright (C) 2005 Vernon Mauery

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/

/**
 * @file
 * Acidfree implements a better photo album than gallery.  Better being
 * defined as doing it The Right Way (TM).  Acidfree was designed as a
 * node based system and written to work seamlessly with Drupal. 
 *
 */

/*
TODO:
 * add some sanity checking too acidfree_cron
 * fix album images to work for various thumb sizes
 * add some help and documentation
 * benchmarking
 * find a way around the drupal_goto in pager path
 * write an xmlrpc hook
 * write some fancy search hooks
 * 
 */

/*
 * WARNING: For modules that perform direct calls to acidfree node_save or
 * node_insert (especially utilities) a call to acidfree_exit() MUST be done
 * after node changes to properly synchronize with filemanager storage areas.
 */

/*
 * Support for Filemanger Private areas: robb@canfield.com
 *
 * Limitations:
 * - Changing a node's permission will invalidate all URLs that
 *   currently reference that node.
 *     - Only applicable when running a mixed public/private environment. If
 *       all content is private then permission changing does not alter URLs.
 */

define('ALBUM_PAGER', 0);
define('ELEMENT_PAGER', 1);
define('PAGER_STRING', "page");
define("IMAGE_SMALL_SIZE",  "640");
define("IMAGE_THUMB_SIZE",  "120");

/**
 * Acidfree Classes
 * Each class must support a basic register function and several
 * callbacks that are used.  Specifically, a class named 'foo'
 * would be implemented in a file called class_foo.inc and be
 * required to implement the following hooks:
 * class_foo_init() - returns an object that has info about the class:
 *      create(&$node, &$file): callback to method to instantiate new element
 *      update(&$node): callback to update element and all its files
 *      destroy(&$node): callback to delete element and all its files
 *      class: name of class
 *      mime_ext: associate array of mime times to extensions this class takes
 *      access: a string describing the access required for creating this class
 * theme_acidfree_print_full_foo(&$node) - called from acidfree_view()
 * theme_acidfree_print_thumb_foo(&$node, &$parent=null, $offset=0)
 *      - called theme_acidfree_print_full_album or the block hook
 */

/**
 * call an arbitrary method with arbitrary arguments
 *
 * note that if you are calling a function that only requires a single arg
 * that is an array, you must pass it in inside of an array like:
 * $bob = array(2,3,4); acidfree_call('chump', array($bob));
 * if you don't chump will be called as chump(2,3,4)
 * which probably isn't what you want
 *
 * passing args by reference is allowed, but you must make the call like:
 * acidfree_call('chump', array(&$foo, $baz));
 * where $foo would be passed by reference and $baz wouldn't.  a call like:
 * acidfree_call('chump', $foo, $baz) will pass by value
 */
function acidfree_call($function) {
    $args = false;
    $argc = func_num_args();
    if ($argc > 2) {
        $args = func_get_args();
        array_shift($args);
    } elseif ($argc != 1) {
        $args = func_get_arg(1);
        if (!is_array($args)) {
            $args = func_get_args();
            array_shift($args);
        }
    }
    if (!$args)
        $args = array();
    if (function_exists($function))
        return call_user_func_array($function, $args);
    return null;
}

/**
 * Implementation of hook_init().
 * 
 * We use this hook to provide us with some variables specific
 * to our module.
 */
function acidfree_init() {
}

/**
 * Implementation of hook_help().
 *
 * Throughout Drupal, hook_help() is used to display help text at the top of
 * pages. Some other parts of Drupal pages get explanatory text from these hooks
 * as well. We use it here to provide a description of the module on the
 * module administration page.
 */
function acidfree_help($section) {
    global $acidfree_types;
    switch ($section) {
        case 'admin/modules#description':
            // This description is shown in the listing at admin/modules.
            if (module_exist('acidfree') && !module_exist('filemanager'))
                drupal_set_message(t("The '%fm' module must be enabled for Acidfree to work.  Please download it from the drupal modules site and enable it below.", Array('%fm'=>'Filemanager')), 'error');
            return t('A better photo album than Gallery or Album. Acidfree is node based and well integrated into Drupal.');
        case 'node/add#acidfree':
            // This description shows up when users click "create content."
            return t('Add an album here.  Other elements you can add are in the \'create content\' menu.');
        case 'admin/help/acidfree':
            return t('<p>Acidfree is an album system that has the ability to store any kind of media (assuming the class is existent.)  Currently implemented are %types.  Following the example of one of the currently implemented classes, it is easy to create a new media type.</p>', array('%types' => implode(', ', array_keys($acidfree_types)))); 
        case 'admin/help#acidfree':
            return t('<p>Because Acidfree is more than simply a storage and viewing module, it requires some depenencies for the album management.  First, it requires the Filemanager module.  Until the Filemanager gets patched to have the exended API that Acidfree requires, Acidfree will ship with a patch for the Filemanager.  Second, it requires some form of image manipulation.  Drupal provides a frontend to the GD library that can do what is required.  Or, if you would rather use ImageMagick or libmagick (php-imagick module), set that up (copy the image.*.inc files to the includes directory) and select it.  Finally, if you want lossless rotations for your images, you will require jpegtran or exiftran.</p><p>For video thumbnailing, there are three options.  You can choose a static image for all your videos (no thumbnail), upload your own thumbnail (user thumbnail) or have mplayer thumbnail them automatically for you.  If you choose the first option, you will be asked to give the dimensions of the video so it can be displayed correctly in the \'full\' view.  With option two, be sure to upload a thumbnail that is the same dimensions as the actual video (grab a frame from the video, do not resize it.)  The third option obviously implies that you have mplayer and all the win32 codecs installed on your <b>server</b> and that you are allowed to exec other programs (i.e., not running in safe mode.).  ');
        case 'admin/settings/acidfree':
            return t('Change these settings to however you like.  You are w00t, afterall.');
        default:
            return "";
            drupal_set_message("help request for acidfree help on $section");
            return "help for $section goes here";
    }
}

/**
 * Implementation of hook_node_info().
 *
 * @return
 *    an associative array with the following keys
 *        $type is the node type
 *        $name is the human readable name of the type
 *        $base is the prefix for hook functions
 */
function acidfree_node_info() {
    $class = _acidfree_get_class_from_path();
    return array('acidfree' => array('name' => t('acidfree %class', array('%class' => $class)), 'base' => 'acidfree'));
}

function acidfree_menu($may_cache) {
    global $acidfree_types, $user;
    $items = array();

    if ($may_cache) {
        $items[] = array('path' => 'acidfree',
                'callback' => 'acidfree_page',
                'title' => t('Acidfree albums'),
                'access' => user_access('access content'),
                'type' => MENU_SUGGESTED_ITEM);
        // acidfree_init stuff
        $create_acidfree_elements = false;
        $have_default = false;
        $acidfree_types = variable_get('acidfree_types', array());
        if (!$acidfree_types) {
            $acidfree_base = drupal_get_path('module', 'acidfree');
            foreach (glob($acidfree_base.DIRECTORY_SEPARATOR.'class_*.inc') as $classfile) {
                require_once($classfile);
                $type = acidfree_call(basename($classfile, ".inc") . "_register");
                if ($type->class)
                    $acidfree_types[$type->class] = $type;
            }
            variable_set('acidfree_types', $acidfree_types);
        }
        foreach ($acidfree_types as $type => $class) {
            $class_access = user_access($class->access);
            $create_acidfree_elements |= $class_access;
            if (!$have_default && $class_access) {
                $have_default = true;
                $menu_type = MENU_DEFAULT_LOCAL_TASK;
            } else {
                $menu_type = MENU_LOCAL_TASK;
            }
            $items[] = array('path' => "node/add/acidfree/$type",
                    'title' => t('Acidfree %class', array('%class' => $type)),
                    'access' => $class_access,
                    'type' => $menu_type);
        }
        $items[] = array('path' => "node/add/acidfree", 'title' => t("acidfree media"),
                'type' => MENU_NORMAL_ITEM | MENU_MODIFIABLE_BY_ADMIN,
                'access' => $create_acidfree_elements);
        $items[] = Array('path' => 'node/add/acidfree/mass', 'title' => t('Mass import'),
                'access' => user_access('acidfree mass import'),
                'callback' => 'acidfree_page',
                'type' => MENU_LOCAL_TASK);
    } else {
        $acidfree_base = drupal_get_path('module', 'acidfree');
        $stylesheet = drupal_get_path('theme', $GLOBALS['theme_key']) . '/acidfree.css';
        if (file_exists($stylesheet)) {
            theme_add_style($stylesheet);
        } else {
            theme_add_style("$acidfree_base/acidfree.css");
        }

        $acidfree_types = variable_get('acidfree_types', array());
        if ($acidfree_types) {
            foreach ($acidfree_types as $name => $class) {
                require_once($acidfree_base.DIRECTORY_SEPARATOR."class_{$name}.inc");
            }
        }

        $path = explode('/', $_GET['q']);
        if ($path[0] == 'node' && is_numeric($path[1])) {
            $node = acidfree_get_node_by_id($path[1]);
            $is_mine = ($node->uid == $user->uid);
            if ($node->class == 'album') {
                $items[] = Array('path' => "node/{$path[1]}/contents",
                        'title' => t('album contents'),
                        'access' => node_access('update', $node),
                        'callback' => 'acidfree_page',
                        'type' => MENU_LOCAL_TASK);
            }
        }
    }
    return $items;
}

function acidfree_settings_validate($form_id, $form_values, $form) {
    acidfree_call('_class_video_manipulation_options_validate', array($form_values));
}

/**
 * Implementation of hook_settings
 */
function acidfree_settings() {
    $form = array();
    $form['#validate'] = array('acidfree_settings_validate' => array());
    $rows = drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    $cols = drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
    $resize_count = drupal_map_assoc(array(10, 20, 50, 100, 200, 500));
    $large_dim = Array(
            -1 => t('no maximum'),
            7990272 => t('8 megapixel (~ 3264x2448)'),
            7004352 => t('7 megapixel (~ 3056x2292)'),
            5947392 => t('6 megapixel (~ 2816x2112)'),
            4915200 => t('5 megapixel (~ 2560x1920)'),
            3763200 => t('4 megapixel (~ 2240x1680)'),
            3145728 => t('3 megapixel (~ 2048x1536)'),
            1920000 => t('2 megapixel (~ 1600x1200)'),
            0 => t('Do not keep large images'),
        );
    $small_dim = Array(1024 => '1024x768', 800 => '800x600', 640 => '640x480', 480 => '480x360', 320 => '320x240');
    $thumb_dim = Array(320 => '320x240', 240 => '240x180', 160 => '160x120', 120 => '120x90', 92 => '92x69', 80 => '80x60');
    $large_size = variable_get('acidfree_large_dim', -1);
    $small_size = variable_get('acidfree_small_dim', IMAGE_SMALL_SIZE);
    $thumb_size = variable_get('acidfree_thumb_dim', IMAGE_THUMB_SIZE);
    $exiftran_path = variable_get('acidfree_path_to_exiftran', '/usr/bin/exiftran');
    $jpegtran_path = variable_get('acidfree_path_to_jpegtran', '/usr/bin/jpegtran');
    if (ini_get('safe_mode')) {
        $safe_dir = ini_get('safe_mode_exec_dir');
        if (dirname($exiftran_path) != $safe_dir && dirname($jpegtran_path) != $safe_dir)
            drupal_set_message(t('Neither exiftran nor jpegtran executables seem to be in safe_mode_exec_dir (%safe_dir) &mdash; lossy jpeg rotation will be used instead', array('%safe_dir' => $safe_dir)));
    }
    if (!is_executable($exiftran_path) && !is_executable($jpegtran_path))
        drupal_set_message(t('Neither exiftran nor jpegtran are available &mdash; lossy jpeg rotation will be used instead'));
    drupal_add_js(drupal_get_path('module', 'acidfree').'/acidfree.js');
    if (variable_get('acidfree_private_process', 0)) {
        variable_set('acidfree_last_resized', 0);
        variable_set('acidfree_private_process', 0);    // Turn off flag in UI
    }
    $last_resized = variable_get('acidfree_last_resized', -1);
    $form['resolution'] = array(
            '#type' => 'fieldset',
            '#title' => t('Image Resolution'),
        );
    $form['resolution']['acidfree_last_resized'] = array(
            '#type' => 'hidden',
            '#default_value' => $last_resized,
            '#id' => 'edit-acidfree_last_resized',
        );
    $form['resolution']['acidfree_resize_count'] = array(
            '#type' => 'select',
            '#title' => t('Items to resize per cron run'),
            '#default_value' => variable_get('acidfree_resize_count', 100),
            '#options' => $resize_count,
            '#description' => t('The maximum amount of items that will be resized in one cron run. Set this number lower if your cron is timing out or if PHP is running out of memory.'),
        );
    if ($last_resized == -1) {
        $status = "No images remaining to resize<br/>\n";
    } else {
        $total = db_result(db_query("SELECT COUNT(aid) AS count FROM {acidfree} WHERE class <> 'album'"));
        if ($total > 0) {
            $to_do = db_result(db_query("SELECT COUNT(aid) AS count FROM {acidfree} WHERE aid>$last_resized AND class <> 'album'"));
            $status .= "{$to_do} of {$total} (".$to_do*100/$total.
                "%) images remaining to resize<br/>\n";
        } else {
            $status = "No images remaining to resize<br/>\n";
        }
    }
    $form['resolution']['status'] = array(
            '#type' => 'markup',
            '#value' => $status,
        );
    $resize_js = "last_resized($large_size, $small_size, $thumb_size, $last_resized);";
    $form['resolution']['acidfree_large_dim'] = array(
            '#type' => 'select',
            '#title' => t("Resolution of full size image"),
            '#default_value' => $large_size,
            '#options' => $large_dim,
            '#attributes' => array("onChange" => $resize_js),
        );
    $form['resolution']['acidfree_small_dim'] = array(
            '#type' => 'select',
            '#title' => t("Resolution of screen size image"),
            '#default_value' => $small_size,
            '#options' => $small_dim,
            '#attributes' => array("onChange" => $resize_js),
        );
    $form['resolution']['acidfree_thumb_dim'] = array(
            '#type' => 'select',
            '#title' => t("Resolution of thumbnail image"),
            '#default_value' => $thumb_size,
            '#options' => $thumb_dim,
            '#attributes' => array("onChange" => $resize_js),
        );
    $form['display'] = array(
            '#type' => 'fieldset',
            '#title' => t('Acidfree Display'),
        );
    $form['display']['acidfree_order'] = array(
            '#type' => 'select',
            '#title' => t('Image order'),
            '#default_value' => variable_get('acidfree_order', 'DESC'),
            '#options' => array('ASC' => t('Chronological'), 'DESC' => t('Most recent first')),
        );
    $form['display']['acidfree_cols'] = array(
            '#type' => 'select',
            '#title' => t('Number of columns'),
            '#default_value' => variable_get('acidfree_cols', 5),
            '#options' => $cols,
        );
    $form['display']['acidfree_rows'] = array(
            '#type' => 'select',
            '#title' => t('Number of rows'),
            '#default_value' => variable_get('acidfree_rows', 3),
            '#options' => $rows,
        );
    $form['display']['acidfree_show_exif_data'] = array(
            '#type' => 'checkbox',
            '#title' => t('Show EXIF data in full image view'),
            '#default_value' => variable_get('acidfree_show_exif_data', false),
        );

    if (!image_get_toolkit())
        drupal_set_message(t('No image toolkit has been properly installed or configured.  Go to admin/settings to fix this.'), 'error');
    $form['image_manip'] = array(
            '#type' => 'fieldset',
            '#title' => t('Image Manipulation'),
        );
    $form['image_manip']['acidfree_path_to_exiftran'] = array(
            '#type' => 'textfield',
            '#default_value' => $exiftran_path,
            '#description' => t('exiftran provides lossless rotation of jpeg images and keeps EXIF information.  If you provide a working path, Acidfree will try to use it.'),
        );
    $form['image_manip']['acidfree_path_to_jpegtran'] = array(
        '#type' => 'textfield',
        '#default_value' => $jpegtran_path,
        '#description' => t('jpegtran provides lossless rotation of jpeg images and can somewhat munge the EXIF information.  If you provide a working path, Acidfree will try to use it.'),
    );
    if (function_exists('_class_video_manipulation_options'))
        $form = array_merge($form, acidfree_call('_class_video_manipulation_options'));

    // Get the number of images in private and public areas
    $result = db_query("SELECT private, count(fid) AS cnt FROM {file} WHERE area = 'acidfree' GROUP BY private");
    $counts = array('%priv' => 0, '%pub' => 0);
    while ( $row = db_fetch_object($result)) {
        if ($row->private) {
            $counts['%priv'] = $row->cnt;
        }
        else {
            $counts['%pub'] = $row->cnt;
        }
    }
    $query = "SELECT count(nid) FROM {node} n INNER JOIN {acidfree} ON {acidfree}.aid = n.nid";
    $counts['%total'] = db_result(db_query($query));
    // The best solution appears to be altering the universal user for the duration of this query.
    global $user;
    if (! variable_get('acidfree_private', 0)) {
        $user_old = $user;
        $user = user_load(array('uid' => 0));
        if ($user) {
            $counts['%na'] = db_result(db_query(db_rewrite_sql($query)));
        }
        else {
            $err = t('ERR: Unable to query for anonymous user, node uid of 0');
            drupal_set_message($err);
            $counts['%na'] = '[' . $err . ']';
        }
        $user = $user_old;
    }
    else {
        $counts['%na'] = $counts['%total'];
    }

    $form['private'] = array(
            '#type' => 'fieldset',
            '#title' => t('Filemanager Settings'),
        );
    $form['private']['text'] = array(
            '#type' => 'markup',
            '#value' => "<p>". t("There are a total of %total Acidfree image nodes. Of these, %na nodes are considered public by acidfree (under node_access protection or acidfree private flag set) as compared to the anonymous user. Filemanager considers %pub acidfree files as public and %priv as private.", $counts) ."</p>",
        );
    $form['private']['acidfree_private'] = array(
            '#type' => 'checkbox',
            '#title' => t("Force all new images to be transferred by Drupal"),
            '#default_value' => variable_get('acidfree_private', 0),
            '#description' => t("If checked all then all images added to acidfree will be streamed through Drupal. Otherwise files are directly accessible via http. If left unchecked then only content under the protection of node_access will be streamed. This setting only applies to added or edited acidfree image nodes. It is usually wise to also process all existing images against this new security, see the next option."),
        );
    $form['private']['acidfree_private_process'] = array(
            '#type' => 'checkbox',
            '#title' => t("Process all images against new security"),
            '#default_value' => variable_get('acidfree_private_process', 0),
            '#description' => t("If checked all then all images in acidfree will be scanned and to adjust their streaming (aka privacy) setting. Some security modules may make changes that require reprocessing all nodes. In general it is a good idea to check this box whenever the above streaming setting is changed, after installing a new security module or changing a security module's settings. The changes are done during the normal cron job for resizing images (and checks for resizing as well) so cron must be configured and you need to wait for the normal cron job time or manually fire the cron job. You may need to clear the Drupal cache to see changes in node URLs."),
        );
    // TODO : Add an option to force an acidfree cron run. This will probably need an http redirect if the run was incomplete

    return $form;
}

/**
 * Implementation of hook_access().
 *
 * Node modules may implement node_access() to determine the operations
 * users may perform on nodes. This example uses a very common access pattern.
 */
function acidfree_access($op, $node) {
    global $user;
    global $acidfree_types;

    if ($op == 'create') {
        // Only users with permission to do so may create this node type.
        if (isset($node->class)) {
            return user_access($acidfree_types[$node->class]->access);
        } else {
            $can_create_something = false;
            foreach ($acidfree_types as $type) {
                $can_create_something |= user_access($type->access);
            }
            return $can_create_something;
        }
    }

    // Users who create a node may edit or delete it later, assuming they have the
    // necessary permissions.
    if ($op == 'update' || $op == 'delete') {
        if (user_access('edit own acidfree elements') && ($user->uid == $node->uid)) {
            return TRUE;
        }
    }
}

/**
 * Implementation of hook_perm().
 *
 * Since we are limiting the ability to create new nodes to certain users,
 * we need to define what those permissions are here. We also define a permission
 * to allow users to edit the nodes they created.
 */
function acidfree_perm() {
    global $acidfree_types;
    $perms = Array();
    foreach ($acidfree_types as $type) {
        $perms[] = $type->access;
    }
    $perms[] = 'edit own acidfree elements';
    $perms[] = 'acidfree mass import';
    $perms[] = 'can upload to any gallery';
    return $perms;
}

/**
 * Implementation of hook_link().
 *
 * This is implemented so that an edit link is displayed for users who have
 * the rights to edit a node.
 */
function acidfree_link($type, $node = 0, $main) {
    $links = array();
    if ($type == 'node' && $node->type == 'acidfree') {
        // Don't display a redundant edit link if they are node administrators.
        if (acidfree_access('update', $node) && !user_access('administer nodes')) {
            // determine whether or not we are actually viewing an album or a node
            $links[] = l(t('edit this %type', array('%type' => $node->class)),
                    "node/$node->nid/edit");
        }
    }
    return $links;
}

function acidfree_get_block_nodes($where, $count) {
    $query = "SELECT nid, aid FROM {node} n ".
             "INNER JOIN {acidfree} ON n.nid = aid ".
             "INNER JOIN {acidfree_hierarchy} ON aid = child ".
             "WHERE n.status=1 AND $where LIMIT $count";
    $query = db_rewrite_sql($query);
    $result = db_query($query);
    $nodes = Array();
    while ($row = db_fetch_object($result)) {
        $nodes[] = acidfree_get_node_by_id($row->aid);
    }
    return $nodes;
}

function acidfree_block_contents($where, $count) {
    $nodes = acidfree_get_block_nodes($where, $count);
    foreach ($nodes as $node) {
        $output .= "<div class='block-cell'>";
        $output .= theme("acidfree_print_thumb_{$node->class}", $node);
        $output .= "</div>";
    }
    $output .= "<div class='block-bottom'>&nbsp;</div>\n";
    return $output;
}

function acidfree_block($op = 'list', $delta = 0, $edit=array()) {
    $titles = array(
        0=>variable_get("acidfree_block_0_title", t('Recent Acidfree items')),
        1=>variable_get("acidfree_block_1_title", t('Favorite Acidfree items')),
        2=>variable_get("acidfree_block_2_title", t('Random Acidfree items'))
    );
    switch ($op) {
    case 'list':
        $blocks[0]['info'] = t('Acidfree item recent selection');
        $blocks[1]['info'] = t('Acidfree item favorite selection');
        $blocks[2]['info'] = t('Acidfree item random selection');
        return $blocks;
    case 'view':
        if (user_access('access content')) {
            switch ($delta) {
                case 0:
                    $where = "class <> 'album' ORDER BY aid DESC";
                    break;
                case 1:
                    $where = "class <> 'album' ORDER BY votes DESC";
                    break;
                case 2:
                default:
                    $all_albums = variable_get('acidfree_block_2_all_albums', 1);
                    $selected_albums = variable_get('acidfree_block_2_random_albums', array());
                    $albums_where = ($all_albums? '' :
                            'parent IN ('.implode(',', $selected_albums).') AND');
                    $where = "$albums_where class <> 'album' ORDER BY RAND()";
                    break;
            }
            $block['subject'] = $titles[$delta];
            $block['content'] = acidfree_block_contents($where,
                    variable_get("acidfree_block_{$delta}_items", 3));
            return $block; 
        }
    case 'configure':
        {
            $items = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20); 
            $fields = array();
            $fields["acidfree_block_{$delta}_items"] = array(
                    '#type' => 'select',
                    '#title' => t('Number of items'),
                    '#default_value' => variable_get("acidfree_block_{$delta}_items", '5'),
                    '#options' => drupal_map_assoc($items),
                );
            $fields["acidfree_block_{$delta}_title"] = array(
                    '#type' => 'textfield',
                    '#title' => t('Title'),
                    '#default_value' => $titles[$delta],
                );
            if ($delta == 2) {
                drupal_add_js(drupal_get_path('module', 'acidfree').'/acidfree.js');
                $all_albums = variable_get('acidfree_block_2_all_albums', 1);
                $fields['acidfree_block_2_all_albums'] = array(
                        '#type' => 'checkbox',
                        '#title' => t('Include items from all albums'),
                        '#default_value' => $all_albums,
                        '#attributes' => array('onchange' => 'toggle_album_select(this);'),
                    );
                $random_selected = variable_get('acidfree_block_2_random_albums', array());
                $fields['acidfree_block_2_random_albums'] = _acidfree_parent_select(
                        t('Select random items from these albums'), $random_selected, null,
                        true, array(), ($all_albums?array('disabled'=>'disabled'):array()));
            }
            return $fields;
        }
    case 'save':
        variable_set("acidfree_block_{$delta}_items", $edit["acidfree_block_{$delta}_items"]);
        variable_set("acidfree_block_{$delta}_title", $edit["acidfree_block_{$delta}_title"]);
        if ($delta == 2) {
            variable_set('acidfree_block_2_random_albums', $edit['acidfree_block_2_random_albums']);
            variable_set('acidfree_block_2_all_albums', $edit['acidfree_block_2_all_albums'] == 1);
        }
        break;
    }
}

function acidfree_album_contents_submit($form_id, $form) {
    $op = isset($_POST['op']) ? $_POST['op'] : '';
    // echo "op is $op<br>\n";
    // echo "<pre>";print_r($form);echo "</pre>";
    $node = $form['album'];
    global $acidfree_types;
    $parent = $form['parent'];
    $targets = array();
    foreach ($form['nodes'] as $fnode) {
        $n = acidfree_get_node_by_id($fnode['nid']);
        // this takes care of class extras and title
        foreach ($fnode as $k => $v) {
            $n->$k = $v;
        }
        if ($fnode['checked'] == '1') {
            // move, copy, delete the node
            switch ($op) {
            case t('Delete'):
                if ($n->class != 'album' && count($n->parent) > 1) {
                    // delete from this album only
                    $key = array_search($node, $n->parent);
                    unset($n->parent[$key]);
                    drupal_set_message("removed '{$n->title}' from album");
                } else {
                    node_delete($n->nid);
                }
                continue;
            case t('Copy'):
                if ($n->class != 'album') {
                    // add dest->nid to parent list
                    $n->parent[] = $parent;
                } else {
                    _acidfree_clone_album($n, $parent);
                }
                break;
            case t('Move'):
                // replace node->nid with dest->nid in parent list
                if ($n->class != 'album') {
                    for ($p=0; $p<count($n->parent); $p++) {
                        if ($n->parent[$p] == $node) {
                            $n->parent[$p] = $parent;
                            break;
                        }
                    }
                } else {
                    $parents = _acidfree_get_ancestors(acidfree_get_node_by_id($parent));
                    $awry = ($parent == $n->nid);
                    foreach ($parents as $p) {
                        if ($p->nid == $n->nid) {
                            $awry = true;
                            break;
                        }
                    }
                    if (!$awry) {
                        $n->parent = array($parent);
                    } else {
                        drupal_set_message("Could not set '{$n->title}' to be own ancestor album", 'error');
                    }
                }
                break;
            }
        }
        node_save($n);
    }
    // not sure why, but even though we pass the nodes around as references,
    // somewhere things go awry and acidfree_get_node_by_id returns the 
    // old copy that is not the one we saved.  so we just reload.
    drupal_set_message(t('Changes saved.'));
    $pager = _acidfree_make_pager_query();
    drupal_goto("node/{$form['album']}/contents", $pager);
}

function _acidfree_form_element($item) {
    $el = '#acidfree_form';
    $item = (array) $item;
    return isset($item['#acidfree_form']);
}
function acidfree_form_elements($arr) {
    return array_filter($arr, '_acidfree_form_element');
}
function theme_acidfree_album_contents($form) {
    if (count($form['nodes']) == 0)
        return "This album is empty<br>";
    $output .= form_render($form['pager']);
    $output .= '<div class="container-inline" style="padding: 5px;">';
    foreach (element_children($form['actionstop']) as $key) {
        $output .= form_render($form['actionstop'][$key]) . '&nbsp;&nbsp;';
    }
    $output .= '</div>';

    foreach (element_children($form['nodes']) as $child) {
        $row[] = Array('data' => form_render($form['nodes'][$child]['checked']));
        $cell = form_render($form['nodes'][$child]['preview']);
        foreach (array_keys(acidfree_form_elements($form['nodes'][$child])) as $key) {
            $cell .= form_render($form['nodes'][$child][$key]);
        }
        $row[] = Array( 'data' => $cell,
                'class' => 'album-cell');
        $cell = '';
        $cell = form_render($form['nodes'][$child]['title']);
        $cell .= form_render($form['nodes'][$child]['body']);
        $cell .= form_render($form['nodes'][$child]['parent']);
        $cell .= form_render($form['nodes'][$child]['weight']);
        $row[] = Array('data' => $cell, 'class' => 'album-cell');
        $cell = form_render($form['nodes'][$child]);
        $row[] = Array('data' => $cell, 'class' => 'album-cell');
        $rows[] = Array('data' => $row);
        unset($row);
    }
    $output .= theme('table', NULL, $rows);
    $output .= '<div class="container-inline" style="padding: 5px;">';
    foreach (element_children($form['actionsbot']) as $key) {
        $output .= form_render($form['actionsbot'][$key]) . '&nbsp;&nbsp;';
    }
    $output .= '</div>';
//    $output .= '<pre>'; $output .= dump($form); $output .= '</pre>';
    return $output.form_render($form);
}

function acidfree_node_form_validate($form_id, &$edit, &$form) {
    if ($form_id != 'acidfree_node_form')
        return;
    if ($form['acidfreeupload'] && !file_check_upload('acidfreeupload'))
        form_error($form['acidfreeupload'], t('No file supplied'));
}

function _acidfree_album_contents_form(&$node) {
    $form = array();
    drupal_set_title(t('Album Contents'));
    drupal_set_breadcrumb($bc = acidfree_make_breadcrumbs($node));
    $ncols = variable_get('acidfree_cols', 5);
    $nrows = variable_get('acidfree_rows', 3);
    $nodes_per_page = $ncols*$nrows;
    $nodes = _acidfree_get_children($node, $nodes_per_page, ALBUM_PAGER);
    if (count($nodes) == 0)
        return "This album is empty<br>";
    $form['album'] = array(
            '#type' => 'hidden',
            '#value' => $node->nid,
        );
    $form['pager'] = array(
            '#value' => theme('pager', NULL, $nodes_per_page, ALBUM_PAGER),
        );
    $form['actionstop']['select'] = array(
            '#type' => 'select',
            '#default_value' => '0',
            '#options' => array('0'=>' -- ', 'all'=>t('All'), 'none'=>t('None'), 'invert'=>t('Invert')),
            '#attributes' => array('onchange' => 'select_nodes(this);'),
            '#prefix' => '<div class="form-item"><label for="edit-select">Select: </label></div>',
        );
    $form['actionstop']['parent'] = _acidfree_parent_select(null, $node->nid, null, false, array(), array('onchange' => 'update_parent_selects(this);'));
    $form['actionstop']['parent']['#prefix'] = '<div class="form-item"><label for="edit-parent">Destination: </label></div>';
    $form['actionstop']['move'] = array(
            '#type' => 'submit',
            '#value' =>t('Move'),
        );
    $form['actionstop']['copy'] = array(
            '#type' => 'submit',
            '#value' =>t('Copy'),
        );
    $form['actionstop']['delete'] = array(
            '#type' => 'submit',
            '#value' =>t('Delete'),
        );
    $form['actionstop']['submit'] = array(
            '#type' => 'submit',
            '#value' =>t('Submit'),
        );

    $form['nodes']['#tree'] = true;
    $i = 0;
    foreach ($nodes as $child) {
        $form['nodes'][$i] = acidfree_form($child);
        $form['nodes'][$i]['preview'] = array(
                '#value' => theme("acidfree_print_thumb_{$child->class}", $child),
            );
        $form['nodes'][$i]['checked'] = array(
                '#type' => 'checkbox',
                '#default_value' => 0,
                '#tree' => true,
            );
        $form['nodes'][$i]['nid'] = array(
                '#type' => 'hidden',
                '#value' => $child->nid,
                '#tree' => true,
            );
        $form['nodes'][$i]['#tree'] = true;
        $form['nodes'][$i]['type']['#value'] = 'acidfree';
        $form['nodes'][$i]['#node'] =& $child;
        foreach (module_implements('form_alter') as $module) {
            $function = $module .'_form_alter';
            $function('acidfree_node_form', $form['nodes'][$i]);
        }
        unset($form['nodes'][$i]['type']);
        $i++;
        continue;
    }
    $form['actionsbot'] = $form['actionstop'];
    return drupal_get_form('acidfree_album_contents', $form);
}

function acidfree_test($argc, $argv) {
    $output = '';//dump($argv);
    if ($argv[0] == 'sanity' || $argc == 0) {
        $errors = false;
        $output .= '<h2>'.t('Acidfree self-test checklist').'</h2><ol>';
        /* filemanager checks */
        if (module_exist('filemanager')) {
            $output .= '<li class="testok">'.t('Filemanager module enabled').'</li>';
            if (function_exists('filemanager_rename')) {
                $output .= '<li class="testok">'.t('Latest version of Filemanager module installed').'</li>';
            } else {
                $output .= '<li class="testfail">'.t('You need to install the latest version of Filemanager for Acidfree to work.  Please download, install, and configure the latest version.').'</li>';
                $errors = true;
            }
            if (function_exists('filemanager_set_private')) {
                $output .= '<li class="testok">'.t('filemanager-private.patch applied').'</li>';
            } else {
                $output .= '<li class="testfail">'.t('filemanager-private.patch <b>not</b> applied.  Acidfree will still work, but your protected nodes\' files will not be protected as well.  Follow the instructions that came with the Acidfree distribution to patch the Filemanager module if you are using a node_access module.').'</li>';
                $errors = true;
            }

            // test filesystem
            $dir = variable_get('filemanager_private_path', 'private');
            if (file_check_directory($dir)) {
                $output .= '<li class="testok">'.t('Filemanager private path set and permissions OK').'</li>';
            } else {
                $output .= '<li class="testfail">'.t('Filemanager private path not properly set or permissions are not right (must be web writeable).  Go to <a href="%url">admin/settings/filemanager</a> to configure it.', array('%url' => url('admin/settings/filemanager'))).'</li>';
                $errors = true;
            }
            $dir = variable_get('filemanager_public_path', 'files');
            if (file_check_directory($dir)) {
                $output .= '<li class="testok">'.t('Filemanager public path set and permissions OK').'</li>';
            } else {
                $output .= '<li class="testfail">'.t('Filemanager public path not properly set or permissions are not right (must be web writeable).  Go to <a href="%url">%url_text</a> to configure it.', array('%url' => url('admin/settings/filemanager'), '%url_text' => 'admin/settings/filemanager')).'</li>';
                $errors = true;
            }
            // test db
            $res = db_query("SELECT fid FROM {file} LIMIT 0");
            if ($res) {
                $output .= '<li class="testok">'.t('Filemanager database table created').'</li>';
            } else {
                $output .= '<li class="testfail">'.t('Filemanager database table needs to be created.  Follow the instructions that came with the filemanager module.').'</li>';
                $errors = true;
            }
        } else {
            $output .= '<li class="testfail">'.t('Filemanager module <b>not</b> enabled.  Go to <a href="%url">%url_text</a> to enabled it.', array('%url' => url('admin/modules'), '%url_text' => 'admin/modules')).'</li>';
            $errors = true;
        }
        /* acidfree filesystem and db checks */
        global $acidfree_types;
        if (is_array($acidfree_types)) {
            $output .= "<li class='testok'>".t("Acidfree class definition files found for %types", array('%types' => implode(', ', array_keys($acidfree_types))))."</li>";
        } else {
            $output .= '<li class="testfail">'.t('Acidfree class definition files not found (class_album.inc, class_photo.inc, etc.)  The entire Acidfree distribution should be untarred in the modules directory &mdash; not just the acidfree.module file.').'</li>';
            $errors = true;
        }
        $dir = dirname(_acidfree_lockfile());
        if (file_check_directory($dir, FILE_CREATE_DIRECTORY)) {
            $output .= '<li class="testok">'.t('Drupal \'Temporary directory\' path set and permissions OK').'</li>';
        } else {
            $output .= '<li class="testfail">'.t('Drupal \'Temporary directory\' path not properly set or permissions are not right (must be web writeable).  Go to <a href="%url">%url_text</a> to configure it.', array('%url' => url('admin/settings'), '%url_text' => 'admin/settings')).'</li>';
            $errors = true;
        }
        $res1 = db_query("SELECT aid FROM {acidfree} LIMIT 0");
        $res2 = db_query("SELECT child FROM {acidfree_hierarchy} LIMIT 0");
        if ($res1 && $res2) {
            $output .= '<li class="testok">'.t('Acidfree database tables created').'</li>';
        } else {
            $output .= '<li class="testfail">'.t('Acidfree database tables needs to be created.  Follow the instructions that that came with the Acidfree distribution.').'</li>';
            $errors = true;
        }
        /* image manipulation checks */
        if (($itk = image_get_toolkit()) && image_toolkit_invoke('check_settings')) {
            $output .= '<li class="testok">'.t('%itk image toolkit installed and properly configured', array('%itk' => $itk)).'</li>';
        } else {
            $output .= '<li class="testfail">'.t('Image toolkit not properly installed or configured.  Go to %url and configure the image toolkit properly.', array('%url' => '<a href="%url">admin/settings</a>', array('%url' => url('admin/settings')))).'</li>';
            $errors = true;
        }
        $output .= "</ol>\n";
        if ($errors) {
            $output .= '<p>'.t('Errors were detected.  Please correct the above errors before you do anything else with Acidfree.')."</p>\n";
        } else {
            $output .= '<p>'.t("No errors were detected.  But this doesn't mean that Acidfree is guaranteed to work flawlessly.  It is a complex system and still under development.  Please be patient while all the wrinkles are found and ironed out.")."</p>\n";
        }
        /* we can do further checks for exiftran, jpegtran, mplayer if we want */
        $max_filesize = ini_get('upload_max_filesize');
        $max_post = ini_get('post_max_size');
        $max_time = ini_get('max_execution_time');
        $max_mem = ini_get('memory_limit');
        $output .= '<h4>'.t('PHP configuration').'</h4><ul>';
        $output .= '<li>'.t('Maximum file upload size is')." $max_filesize (upload_max_filesize)</li>";
        $output .= '<li>'.t('Maximum POST size is')." $max_post (post_max_size)</li>";
        $output .= '<li>'.t('Maximum execution time is %max_time seconds', array('%max_time' => $max_time)).' (max_execution_time)</li>';
        $output .= '<li>'.t('Maximum memory usage is')." $max_mem (memory_limit)</li>";
        $output .= '</ul><p>'.t('If these limits are smaller than the files you plan on uploading, you should change the settings in your php.ini file').'</p>';
    } elseif ($argv[0] == 'list') {
        $output .= 'sanity, list<br>';
    } else {
        drupal_goto('acidfree');
    }
    return $output;
}

function acidfree_page() {
    $argc = func_num_args();
    $argv = func_get_args();
    $path = _acidfree_get_path(true);
    drupal_add_js(drupal_get_path('module', 'acidfree').'/acidfree.js');
    if (strstr($path, 'node/add/acidfree/mass')) {
        if ($argc == 1)
            $parent = acidfree_get_node_by_id($argv[0]);
        else
            $parent = acidfree_get_root(false);
       $output .= _acidfree_import_form($parent); 
    } elseif (preg_match('@node/([0-9]*)/contents@', $path, $matches)) {
        $node = acidfree_get_node_by_id($matches[1]);
        drupal_set_title(t('Album Contents'));
        drupal_set_breadcrumb(acidfree_make_breadcrumbs($node));
        $output .= _acidfree_album_contents_form($node);
    } elseif ($argc == 0) {
        $node = acidfree_get_root(false);
        drupal_goto("node/{$node->nid}");
    } elseif ($argv[0] == 'test') {
        array_shift($argv);
        if(! user_access('administer site configuration')) {
          return drupal_access_denied();
        }
        $output .= acidfree_test(--$argc, $argv);
    } else {
        drupal_not_found();
        //gotto(__LINE__);
        return;
    }
    return $output;
}

/**
 * Implementation of hook_view().
 */
function acidfree_view(&$node, $teaser=FALSE, $page=FALSE) {
    /* we expect paths of the sort...
       node/aaaa          -- view an album
       node/nnnn          -- view a node (choose default album)
       node/nnnn?pid=aaaa -- view a node in an album
       node/nnnn/edit     -- edit a node (album or node) well, this doesn't really come here...
       node/aaaa/contents -- edit the contents of an album
     */
    if ($teaser) {
        $node = node_prepare($node, $teaser);
        $output .= '<div class="block-cell">';
        $output .= theme("acidfree_print_thumb_{$node->class}", $node);
        $output .= '</div>' . $node->teaser;
        $node->teaser = $output;
    } else {
        if ($node->class != 'album') {
            $node->album = _acidfree_get_parent_from_path();
        }
        _acidfree_forward($node);
        if ($page) {
            acidfree_add_vote($node);
            drupal_set_breadcrumb(acidfree_make_breadcrumbs($node));
        }
        $node = node_prepare($node, $teaser);
        $node->body = theme("acidfree_print_full_{$node->class}", $node);
    }
}

/**
 * Implementation of hook_form().
 *
 * Now it's time to describe the form for collecting the information
 * specific to this node type. This hook requires us to return some HTML
 * that will be later placed inside the form.
 */
function acidfree_form(&$node) {
    drupal_add_js(drupal_get_path('module', 'acidfree').'/acidfree.js');
    $form = array();
    $form['title'] = array(
            '#type' => 'textfield',
            '#title' => t('Title'),
            '#size' => 60,
            '#maxlength' => 128,
            '#required' => TRUE,
            '#default_value' => $node->title,
            );
    $form['body'] = array(
            '#type' => 'textarea',
            '#title' => t('Description'),
            '#default_value' => $node->body,
            '#required' => FALSE,
            );

    if ($node != null)
        drupal_set_breadcrumb(acidfree_make_breadcrumbs($node));
    if (!isset($node->class)) {
        $form = array_merge($form, _acidfree_new_form($node));
        $form['#attributes'] = array('enctype' => 'multipart/form-data');
    } else {
        $form = array_merge($form, _acidfree_update_form($node));
    }
    // these things are common to new and used acidfrees
    $form['class'] = array(
            '#type' => 'hidden',
            '#value' => $node->class,
        );
    // parent
    if ($node->nid != acidfree_get_root()) {
        $path = _acidfree_get_path();
        if ($path[0] == 'node' && $path[1] == 'add'
                && $path[2] == 'acidfree' && is_numeric(arg(4))) {
            $node->parent = Array(arg(4));
        }
        $children = acidfree_get_album_tree($node);

        // An album can't be the child of itself, nor of its children.
        foreach ($children as $child) {
            $exclude[] = $child->nid;
        }
        $exclude[] = $node->nid;

        if ($node->class == 'album') {
            $form['parent'] = _acidfree_parent_select(t('Parent album'), $node->parent,
                l(t('Parent album'), 'admin/help/acidfree', NULL, NULL, 'parent') .'.', 0, $exclude);
        } else {
            $form['parent'] = _acidfree_parent_select(t('Parent albums'), $node->parent,
                l(t('Parent albums'), 'admin/help/acidfree', NULL, NULL, 'parent') .'.', 1, $exclude);
        }
    } else {
        $form['parent'] = array(
                '#type' => 'hidden',
                '#value' => -1,
            );
    }
    $form['body'] = array(
            '#type' => 'textarea',
            '#title' => t('Description'),
            '#default_value' => $node->body,
            '#rows' => 5,
            '#cols' => 50,
            '#required' => false,
            '#weight' => 10,
        );
        
    $woffset = ($node->class == 'album' ? -30 : 0);
    for ($i=-10; $i<=10; $i++) {
        $weights[$i+$woffset] = $i;
    } 
    $form['weight'] = array(
        '#type' => 'weight',
        '#title' => t('Weight'),
        '#default_value' => $node->weight,
        '#delta' => 15,
        '#weight' => 14,
        '#description' => t('The weight of a node determines its relative position with other nodes &mdash; heavier nodes sink to the bottom, or end of the listings, while lighter nodes rise to the top, or beginning'),
    );

    return $form;
}

/**
 * Implementation of hook_validate().
 */
function acidfree_validate(&$node) {
    echo "acidfree_validate called for with <br>";
    $args = func_get_args();
    // FIXME: make sure the updates to this node are valid

    node_validate_title($node);

    // FIXME: this is a hack to force the validator that the file is okay.
    // First, we really should check that it is.  Second, there is probably
    // a better way to tell the validator than modifying the $_POST variable.
    if (isset($_POST['edit']))
        $_POST['edit']['acidfreeupload'] = 'file';
}

function _acidfree_create_root() {
    $node = Array(
            'title' => 'Acidfree Albums',
            'body' => '',
            'teaser' => '',
            'uid' => 1,
            'class' => 'album',
            'type' => 'acidfree',
            'status' => 1,
            'weight' => -30,
            'parent' => Array(-1),
        );
    $node = (object)$node;
    node_save($node);
    return $node->nid;
}

function acidfree_get_root($id_only=true) {
    static $root_id = null;
    if ($root_id == null) {
        $result = db_query("SELECT child FROM {acidfree_hierarchy} WHERE parent='-1'");
        $result = _acidfree_fix_root($result);
        $row = db_fetch_array($result);
        $root_id = $row['child'];
    }
    if ($id_only)
        return $root_id;
    return acidfree_get_node_by_id($root_id);
}

function acidfree_get_album_tree(&$node, $depth=0) {
    $tree = Array();
    if (!isset($node->nid))
        return $tree;
    $children = _acidfree_get_child_albums($node);
    foreach ($children as $child) {
        $child->depth = $depth;
        $tree[] = $child;
        $tree = array_merge($tree, acidfree_get_album_tree($child, $depth + 1));
    }
    return $tree;
}

function acidfree_add_vote(&$node) {
    if (!isset($node->nid))
        return;
    $query = "UPDATE {acidfree} SET votes=(votes+1) WHERE aid={$node->nid}";
    db_query($query);
}

/**
 * Implementation of hook_insert().
 *
 * As a new node is being inserted into the database, we need to do our own
 * database inserts.
 */
function acidfree_insert(&$node) {
    global $ACIDFREE_SYNC_PRIVATE;
    global $acidfree_types;
    if ($node->class != 'album') {
        if (isset($_SESSION['acidfreeupload'])) {
            $file = $_SESSION['acidfreeupload'];
            unset($_SESSION['acidfreeupload']);
        } else {
            $file = 'acidfreeupload';
        }
        $file = file_check_upload($file);
        $file->filemime = acidfree_mime($file);
        acidfree_call($acidfree_types[$node->class]->create, array(&$node, &$file));
    } else {
        acidfree_call($acidfree_types['album']->create, array(&$node, &$file));
    }
    // promote the files from working to active
    foreach (Array('large', 'small', 'thumb') as $size) {
        $fid = (is_object($node->$size) ? $node->$size->fid : $node->$size);
        $file = module_invoke('filemanager', 'get_file_info', $fid);
        if ($node->ispreview) {
            $ext = $acidfree_types[$node->class]->mime_ext[$file->mimetype];
            $file = module_invoke('filemanager', 'rename', $file, "{$node->nid}_{$size}.{$ext}");
        }
        module_invoke('filemanager', 'promote_working', $file);
        $node->$size = $fid;
    }
    db_query("INSERT INTO {acidfree} (aid, class, large, small, thumb, weight) ".
             "VALUES (%d, '%s', '%s', '%s','%s', %d)",
             $node->nid, $node->class, $node->large, $node->small, $node->thumb, $node->weight);
    if (!isset($node->parent)) {
        $node->parent = Array(acidfree_get_root());
    } elseif (!is_array($node->parent)) {
        $node->parent = Array($node->parent);
    }
    foreach ($node->parent as $p) {
        db_query("INSERT INTO {acidfree_hierarchy} (child, parent) VALUES (%d, %d)", $node->nid, $p);
    } 

    $ACIDFREE_SYNC_PRIVATE[$node->nid] = 1;
}

/**
 * Implementation of hook_update().
 *
 * As an existing node is being updated in the database, we need to do our own
 * database updates.
 */
function acidfree_update(&$node) {
    global $ACIDFREE_SYNC_PRIVATE;
    global $acidfree_types;

    // Parent linking is complicated by possible node_access modules. Make sure that all parent links
    // specified are valid for the current user. If ANY are invalid it means that the Drupal form was bypassed
    // so ignore them. A parent of -1 or blank will retain the existing parent structure.
    $parent = $node->parent;
    $valid = array();
    if ($parent > -1) {
      // Get albums this user has access to
      $root = acidfree_get_root(false);
      if ($root->nid) {
        $valid[$root->nid] = 1;
      }
      $tree = acidfree_get_album_tree($root);
      if ($tree) {
        foreach ($tree as $album) {
          // TODO: I am not sure if duplicates are ever possible, but assume they are and drop them as the come
          $valid[$album->nid] = 1;
        }
      }

      // Normalize parent data
      if (!is_array($parent)) {
        $parent = array($parent);
      }

      // Make sure parents are a subset of what is allowed to this user
      $matches = array_intersect(array_keys($valid), $parent);
      if (count($matches) <> count($parent)) {
        $parent = -1;
      }
      else {
        $node->parent = $parent;
      }
    }

    // Handle cases where parent list was invalid or the user did not have valid access to it
    if($parent == -1 || empty($parent) || ! $parent) {
      $update_parent = FALSE;
    }
    else {
      $update_parent = TRUE;
    }

    $updates = acidfree_call($acidfree_types[$node->class]->update, array(&$node));
    db_query("UPDATE {acidfree} SET $updates WHERE aid = %d", $node->nid);
    if ($update_parent) {
      db_query("DELETE FROM {acidfree_hierarchy} WHERE child = %d", $node->nid);
      if (!is_array($node->parent))
          $node->parent = Array($node->parent);
      foreach ($node->parent as $parent) {
          db_query("INSERT INTO {acidfree_hierarchy} (child, parent) VALUES (%d, %d)",
                  $node->nid, $parent);
      }
    }

    $ACIDFREE_SYNC_PRIVATE[$node->nid] = 1;
}

/**
 * Implementation of hook_delete().
 *
 * When a node is deleted, we need to clean up related tables.
 */
function acidfree_delete(&$node) {
    global $acidfree_types;
    acidfree_call($acidfree_types[$node->class]->destroy, array(&$node));
    db_query("DELETE FROM {acidfree} WHERE aid = %d", $node->nid);
    db_query("DELETE FROM {acidfree_hierarchy} WHERE child = %d", $node->nid);
    // clean up our files
    if ($node->class != 'album') {
        foreach (Array('large','small','thumb') as $size) {
            $file = $node->$size;
            module_invoke('filemanager', 'delete', $file);
        }
    }
    // take care of orphaned album thumbs
    db_query("UPDATE {acidfree} SET thumb='' WHERE thumb='%s'", $node->thumb);
}

/**
 * Implementation of hook_load().
 *
 * Now that we've defined how to manage the node data in the database, we
 * need to tell Drupal how to get the node back out. This hook is called
 * every time a node is loaded, and allows us to do some loading of our own.
 */
function acidfree_load(&$node) {
    $additions = db_fetch_object(db_query("SELECT class,large,small,thumb,weight,visited,votes FROM {acidfree} WHERE aid = %d", $node->nid));
    $result = db_query("SELECT parent FROM {acidfree_hierarchy} WHERE child = %d", $node->nid);
    while ($p = db_fetch_object($result)) {
        $parents[] = $p->parent;
    }
    $additions->parent = $parents;
    if ($additions->class == 'album')
        $additions->weight += 30;

    return $additions;
}

/**
 * implement the hook_filter method
 */
function acidfree_filter($op, $delta = 0, $format = -1, $text = '') {
    switch ($op) {
    case 'list':
        return array(0 => t('Acidfree inline filter'));
    case 'no cache':
        return true;
    case 'description':
        return 'add acidfree images to other nodes using a syntax like [acidfree:xx ...]';
    case 'prepare':
        return $text;
    case 'process':
        // look for things like [acidfree:1377 style=slide inline=true]
        // look for things like [acidfree:1377 style=slide align=left]
        // look for things like [acidfree:1377 style=image align=right size=320x240]
        return preg_replace_callback("@\[acidfree:([0-9]*)([^\\]]*)\]@i",
                        '_acidfree_filter_tag', $text);
    case 'settings':
        return '';
    default:
        return $text;
    }
}

function acidfree_filter_tips($delta, $format, $long = false) {
  if ($long) {
    return t('
    <p>You may link to existing acidfree images and videos to the current node using special tags. The tags will be replaced by the corresponding media. For example:

    Suppose you have acidfree nodes 123, 234, 345.
    
    <pre>[acidfree:123]</pre>

    will be replaced by <em><code>&lt;img src=\'path/to/acidfree/image/123\'&gt;</code></em>

    ');
  }
  else {
    return t('You may use <a href="%acidfree_help">[acidfree:xx] tags</a> to display acidfree videos or images inline.', array("%acidfree_help" => url("filter/tips/$format", NULL, 'filter-acidfree')));
  }
}

function acidfree_cron() {
    // call the init function
    acidfree_menu(false);
    // Resize code
    $count = variable_get('acidfree_resize_count', 50);
    $last_resized = variable_get('acidfree_last_resized', -1);
    if ($last_resized < 0)
        return;
    // Make sure aids are processed in a sorted order, otherwise it is possible (likely) that some nodes would be missed
    $query = "SELECT aid FROM {acidfree} ".
             "WHERE class <> 'album' && aid>$last_resized ".
             "ORDER BY aid LIMIT $count";
    $result = db_query($query);
    if (($to_do = db_num_rows($result)) < 1) {
        variable_set('acidfree_last_resized', -1);
        return;
    }
    while ($item = db_fetch_array($result)) {
        global $acidfree_types;
        $node = acidfree_get_node_by_id($item['aid']);
        acidfree_sync_private($node);

        if (!acidfree_call($acidfree_types[$node->class]->resize, array(&$node)))
            watchdog("Acidfree", "failed to resize acidfree node {$node->nid} ({$node->title})", WATCHDOG_NOTICE, l('edit', "node/{$node->nid}/edit")." ".l('view', "node/{$node->nid}"));
    }
    if ($to_do < $count)
        variable_set('acidfree_last_resized', -1);
    else
        variable_set('acidfree_last_resized', $node->nid);
}

function &acidfree_make_breadcrumbs(&$node) {
    $crumbs = Array(l(t('Home'), NULL));
    $parents = _acidfree_get_ancestors($node);
    if (count($parents) > 0) {
        $crumbs[] = l(t('Albums'), 'acidfree');
    }
    if (count($parents) > 1) {
        foreach ($parents as $parent) {
            if ($parent->nid != acidfree_get_root())
            $crumbs[] = l($parent->title, "node/{$parent->nid}");
        }
    }
    return $crumbs;
}

function _acidfree_clone_album(&$node, $parent) {
    // create a new album node the same as node
    $n = $node;
    $n->nid = 0;
    $n->parent = array($parent);
    node_save($n);
    // collect all the children and copy them in too
    $children = _acidfree_get_children($node);
    foreach ($children as $child) {
        if ($child->class != 'album') {
            // add dest->nid to parent list
            $child->parent[] = $n->nid;
            node_save($child);
        } else {
            _acidfree_clone_album($child, $n->nid);
        }
    }
}

function _acidfree_node_from_file($parent, &$file) {
    $node = new stdClass();
    $node->type = 'acidfree';
    $time = filemtime($file->filepath);
    if (!$time) {
        $time = time();
    }
    $node->created = $node->updated = $time;
    if (is_dir($file->filepath)) {
        $node->class = 'album';
        $node->weight = -20;
        $node->title = $file->filename;
    } else {
        $node->weight = 0;
        global $acidfree_types;
        foreach ($acidfree_types as $type) {
            if (in_array($file->filemime, array_keys($type->mime_ext))) {
                $node->class = $type->class;
                break;
            }
        }
        if (!isset($node->class)) {
            drupal_set_message(t('file %filename is not a valid acidfree element type (%mime)', Array('%filename' => $file->filename, '%mime'=>$mime)), 'error');
            return false;
        }
        $node->title = acidfree_title_from_file($file->filename);
    }
    // echo "creating {$node->class} {$node->title}<br>\n";
    $node->body = $node->teaser = '';
    $node_options = variable_get('node_options_acidfree', array('status', 'promote'));
    $node->comment = variable_get('comment_acidfree', 0);
    $node->promote = in_array('promote', $node_options);
    $node->status = in_array('status', $node_options);
    $node->moderate = in_array('moderate', $node_options);
    $node->sticky = in_array('sticky', $node_options);
    $node->published = in_array('published', $node_options);
    global $user;
    $node->uid = $user->uid;
    $node->parent = Array($parent->nid);
    return $node;
}

function _acidfree_object_merge($base, $obj) {
    if (empty($obj))
        return $base;
    foreach ($obj as $k => $v)
        if (!isset($base->k))
            $base->$k = $v;
    return $base;
}

function _acidfree_import_dir($parent, $path, $recursive, $node_items) {
    $dirs = Array();
    // echo "opendir($path)<br>\n";
    $dir = opendir($path);
    while ($filename = readdir($dir)) {
        if ($filename == '.' || $filename == '..')
            continue;
        $fullpath = "$path/$filename";

        $file = new stdClass();
        $file->source = 'acidfree_local_filesystem';
        $file->filepath = $fullpath;
        $file->filename = $filename;

        // echo "importing file $filename<br>\n";
        if ($recursive && is_dir($fullpath)) {
            $dirs[] = $file;
            continue;
        }
        $file->filemime = acidfree_mime($file);
        $_SESSION['acidfreeupload'] = $file;
        $node = _acidfree_node_from_file($parent, $file);
        if (!$node)
            continue;
        $node = _acidfree_object_merge($node, $node_items);
        node_save($node);
        if (!$node->nid) {
            drupal_set_message("failed to import '$filename'", 'error');
        }
    }
    closedir($dir);
    foreach ($dirs as $dir) {
        if (!($cur_album = _acidfree_node_from_file($parent, $dir)))
            continue;
        $cur_album = _acidfree_object_merge($cur_album, $node_items);
        node_save($cur_album);
        if (!$cur_album->nid) {
            drupal_set_message("failed to import '{$dir->filename}'", 'error');
            continue;
        }
        if ($recursive) {
            _acidfree_import_dir($cur_album, $dir->filepath, $recursive, $node_items);
        }
    }
}

function _acidfree_handle_uploaded_file(&$file, &$parent, $node_items) {
    // We just can't seem to trust the mimetypes that browers
    // give us, so we are forced to use our own mime code every time
    $file->filemime = acidfree_mime($file);
    // FIXME: this may be a cause for alarm -- uploading archives
    // that contain files of the same path/name will cause collisions.
    // Find a way to avoid this
    switch ($file->filemime) {
        case 'application/zip':
        case 'application/x-zip':
        case 'application/x-zip-compressed':
            $cmd = "unzip -qq {$file->filepath} -d %aftmpdir";
            break;
        case 'application/x-bzip2':
        case 'application/x-bzip':
            $cmd = "tar -C %aftmpdir -xjf {$file->filepath}";
            break;
        case 'application/gzip':
        case 'application/x-gzip':
            $cmd = "tar -C %aftmpdir -xzf {$file->filepath}";
            break;
        default:
            $_SESSION['acidfreeupload'] = $file;
            $node = _acidfree_node_from_file($parent, $file);
            if (!$node)
                return;
            $node = _acidfree_object_merge($node, $node_items);
            node_save($node);
            if (!$node->nid) {
                drupal_set_message("failed to import {$file->filename}", 'error');
            }
            break;
    }
    // extract the archive
    if (strlen($cmd) > 0) {
        $tmpdir = acidfree_mktmpdir("massupload");
        if (!$tmpdir) {
            drupal_set_message(t('Could not create temporary directory for mass import'), 'error');
            return;
        }
        $cmd = preg_replace('/%aftmpdir/', $tmpdir, $cmd);
        system($cmd);
        _acidfree_import_dir($parent, $tmpdir, true, $node_items);
        rmdir_rec($tmpdir);
    }
}

function acidfree_mass_import_submit($form_id, $form) {
    $op = isset($_POST['op']) ? $_POST['op'] : '';
    $extime = ini_set('max_execution_time', 600);
    //echo "mass upload<br>\n";
    //echo "<pre>"; print_r($form); echo "</pre>";
    $recursive = isset($form['recursive']) ? $form['recursive'] : true;
    $node_items = $form['node_items'];
    $p = acidfree_get_node_by_id($form['parent']);
    if ($op == t('Import')) {
        $tmpdir = $form['path'];
        if ($tmpdir && !is_dir($tmpdir)) {
            drupal_set_message(t('Could not find server-side directory %tmpdir', array('%tmpdir' => $tmpdir)), 'error');
        } elseif ($tmpdir) {
            drupal_set_message("Importing server side directory $tmpdir");
            _acidfree_import_dir($p, $tmpdir, $recursive, $node_items);
        }
    } elseif ($op == t('Upload')) {
        $i=0;
        while ($file = file_check_upload("acidfreeupload$i")) {
            drupal_set_message("Importing {$file->filename}");
            _acidfree_handle_uploaded_file($file, $p, $node_items);
            $i++;
        }
    }
    ini_set('max_execution_time', $extime);
    drupal_goto("node/{$form['parent']}");
}
function _acidfree_import_form(&$parent) {
    $form = array();
    $form['parent'] = _acidfree_parent_select(t('Parent album'), $parent->nid,
            t('Select the album to place this upload in.'), false);
    $form['node_items'] = node_invoke_nodeapi($parent, 'form');
    $form['node_items']['#tree'] = true;
    $form['upload'] = array(
            '#type' => 'fieldset',
            '#title' => t('Files to upload'),
        );
    $form['upload']['acidfreeupload0'] = array(
            '#type' => 'file',
            '#title' => t('File to upload'),
            '#size' => '50',
            '#description' => t('File to upload.  This may be an image, video clip, zip file, or a tar file that is gzipped or bzipped.  You may upload up to five at a time.'),
        );
    $file = array(
            '#type' => 'file',
            '#size' => '50',
        );
    $form['upload']['acidfreeupload1'] = $file;
    $form['upload']['acidfreeupload2'] = $file;
    $form['upload']['acidfreeupload3'] = $file;
    $form['upload']['acidfreeupload4'] = $file;
    $form['upload']['upload'] = array(
            '#type' => 'submit',
            '#value' => t('Upload'),
        );
    $form['import'] = array(
            '#type' => 'fieldset',
            '#title' => t('Import local files'),
            '#collapsible' => true,
            '#collapsed' => true,
        );
    $form['import']['path'] = array(
            '#type' => 'textfield',
            '#title' => t('Path on server'), 
            '#description' => t('This must be the full path to a directory on the server that is readable.'),
        );
    $form['import']['recursive'] = array(
            '#type' => 'checkbox',
            '#title' => t('Include subdirectories'),
            '#default_value' => 1,
        );
    $form['import']['import'] = array(
            '#type' => 'submit',
            '#value' => t('Import'),
        );
    $form['#attributes'] = array('enctype' => 'multipart/form-data');
    return drupal_get_form('acidfree_mass_import', $form);
}

function _acidfree_update_form(&$node) {
    global $acidfree_types;
    $form = array();
    if ($file = file_check_upload('acidfreeupload')) {
        $node->nid = 'tmp'.rand();
        acidfree_call($acidfree_types[$node->class]->create, array(&$node, &$file));
        $node->ispreview = 1;
        unset($node->nid);
    }
    $size = variable_get('acidfree_thumb_dim', IMAGE_THUMB_SIZE) + 30;
    $thumb .= "<div style='width: {$size}px; margin: 0px; padding: 0px;'>";
    $thumb .= theme("acidfree_print_thumb_{$node->class}", $node, null, 0);
    $thumb .= '</div>';
    $form['preview'] = array(
            '#value' => $thumb,
        );
    // we don't include thumb because extras includes the thumb
    $form['small'] = array(
            '#type' => 'hidden', 
            '#value' => is_object($node->small)?$node->small->fid:$node->small,
        );
    $form['large'] = array(
            '#type' => 'hidden',
            '#value' => is_object($node->large)?$node->large->fid:$node->large,
        );
    if (!isset($node->ispreview)) {
        $form = array_merge($form, acidfree_call($acidfree_types[$node->class]->form, array('update', &$node)));
    } else {
        $form = array_merge($form, acidfree_call($acidfree_types[$node->class]->form, array('preview', &$node)));
        $form['ispreview'] = array(
                '#type' => 'hidden',
                '#value' => 1,
            );
    }
    return $form;
}

function _acidfree_new_form(&$node) {
    $node->class = _acidfree_get_class_from_path();
    $form = array();
    if ($node->class == 'album') {
        $node->weight = -30;
    } else {
        $form['acidfreeupload'] = array(
                '#type' => 'file',
                '#title' => $node->class,
                '#required' => true,
                '#size' => '50',
                '#attributes' => array('onchange' => 'set_title(this.value);'),
                '#description' => t('This is the path to the %class that you are uploading.  Be sure that it fits within the specifications for file size and type.', array('%class' => $node->class)),
                '#value' => 'dummy_value',
            );
    }
    global $acidfree_types;
    $form = array_merge($form, acidfree_call($acidfree_types[$node->class]->form,
        array('new', &$node)));
    return $form;
}

// this is a wacky function that accepts a possibly broken
// root album query result and returns a fixed one
function _acidfree_fix_root($result) {
    $count = db_num_rows($result);
    switch ($count) {
    case 1:
        return $result;
    case 0:
        $root = _acidfree_create_root();
        $msg = "No Acidfree root album found, created new root -- node $root";
        $wd = WATCHDOG_NOTICE;
        $ml = 'status';
        break;
    default:
        $node = new stdClass();
        $node->nid = -1;
        $items = _acidfree_get_children($node);
        $children = Array();
        foreach ($items as $item) {
            $children[] = $item->nid;
        }
        $children = implode(",", $children);
        $root = _acidfree_create_root();
        db_query("UPDATE {acidfree_hierarchy} SET parent=$root WHERE parent=-1 AND child IN ($children)");
        $msg = "Found $count Acidfree root albums, new root is node $root";
        $wd = WATCHDOG_ERROR;
        $ml = 'error';
        break;
    }
    watchdog("Acidfree", $msg, $wd, l('edit', "node/$root/edit")." ".l('view', "node/$root"));
    drupal_set_message($msg, $ml);
    return db_query("SELECT child FROM {acidfree_hierarchy} WHERE parent='-1'");
}

function _acidfree_filter_tag($matches) {
    $node = acidfree_get_node_by_id($matches[1]);
    if (!$node) {
        return "<span style='color: #f00;'>{$matches[0]}</span>";
    } else {
        $args = preg_replace("/[ \\n\\t]+/", ' ', $matches[2]);
        $args = explode(" ", $args);
        $tsize = variable_get('acidfree_thumb_dim', IMAGE_THUMB_SIZE);
        $settings = Array('align'=>'left','size'=>"{$tsize}x{$tsize}",'style'=>'image');
        foreach ($args as $arg) {
            $arg = explode('=', $arg);
            $settings[strtolower(trim($arg[0]))] = strtolower(trim($arg[1]));
        }
        $ret = "<span style='float: {$settings['align']}; padding: 0px; margin: 0px 5px 0px 5px; vertical-align: middle;'>";
        if ($settings['style'] != 'slide') {
            $size = explode('x', $settings['size']);
            $width = $size[0]; $height = ($size[1] ? $size[1] : $size[0]);
            if (max($width, $height) > $tsize) {
                $filepath = _acidfree_get_small_path($node);
                $fileurl = _acidfree_get_small_url($node);
            } else {
                $filepath = _acidfree_get_thumb_path($node);
                $fileurl = _acidfree_get_thumb_url($node);
            }
            $f_dim = getimagesize($filepath);
            $f_width = $f_dim[0]; $f_height = $f_dim[1];
            if ($f_width > $f_height)   {
                $style = "width: {$width}px;";
            } else {
                $style = "height: {$height}px;";
            }
            $ret .= l(theme('image', $fileurl, $node->title, $node->title, 
                        "class='acidfree-plain' style='$style'", false),
                    "node/{$node->nid}{$p}{$from}", NULL, NULL, NULL, FALSE, TRUE);
        } else {
            $ret .= theme("acidfree_print_thumb_{$node->class}", $node);
        }
        $ret .= "</span>";
        return $ret;
    }
}

function _acidfree_parent_select($title, $value, $description, $multiple, $exclude = array(), $extra = array()) {
    $root = acidfree_get_root(false);
    $tree = acidfree_get_album_tree($root);
    if ($root->nid) {
      $options[$root->nid] = '<'.t('Root').'>';
    }

    if ($tree) {
        // if the user cannot upload to *any* album (other than his own),
        // then exclude all albums he did not create
        if (!user_access('can upload to any gallery')) {
          global $user;
          $query = "SELECT n.nid FROM {node} n
            LEFT JOIN {acidfree} a
            ON n.nid = a.aid
            WHERE a.class='album' AND n.uid!=$user->uid";
          $resource = db_query($query);
          while ($result = db_fetch_array($resource)){
            $exclude[] = $result['nid'];
          }
        }

        foreach ($tree as $album) {
            if (!in_array($album->nid, $exclude)) {
                $options[$album->nid] = str_repeat('-', $album->depth) . $album->title;
            }
        }
        if (!$value) {
            $value = $root->nid;
        }
    }

    if ($options) {

        if ($multiple)
            $extra['size'] = min(6, count($options));

        return Array(
                '#type' => 'select',
                '#title' => $title,
                '#options' => $options,
                '#default_value' => $value,
                '#multiple' => $multiple,
                '#attributes' => $extra,
            );
    } else {
      return '';
    }
}

function &acidfree_get_node_by_id($nid) {
    static $nodes;
    // FIXME:  why is this getting called with $nid == null?
    if ($nid == null)
        return false;
    if (!isset($nodes[$nid])) {
        $node = node_load($nid);
        // Added code to check permission - Filters (via db_rewrite_sql) should have already filtered inaccessible records
        // but this acts as a final security check (with almost no hit on performance).
        if (!$node->nid || $node->type != 'acidfree' || ! node_access('view', $node))
            return false;

        $nodes[$nid] =& $node;
    }
    return $nodes[$nid];
}

function &_acidfree_get_children(&$node, $limit=-1, $pagerid=0, $no_albums=false) {
    if (! $node->nid) {
      return array();
    }

    if ($no_albums) {
        $where_class = "AND class <> 'album'";
    }
    // Create count query. Drupal's pager function does not build the query properly
    $clauses = "FROM {node} n INNER JOIN {acidfree} on n.nid = {acidfree}.aid ". 
        "INNER JOIN {acidfree_hierarchy} ON {acidfree_hierarchy}.child = {acidfree}.aid ".
        "WHERE parent = {$node->nid} $where_class ".
        _acidfree_content_sort_clause();
    $query = db_rewrite_sql("SELECT n.nid " . $clauses);
    $count_query = db_rewrite_sql("SELECT count(n.nid) " . $clauses);
    if ($limit == -1) {
        $kids = db_query($query);
    } else {
        $kids = pager_query($query, $limit, $pagerid, $count_query);
    }
    $children = array();
    while ($kid = db_fetch_array($kids)) {
        $children[] = acidfree_get_node_by_id($kid['nid']);
    }
    return $children;
}

function _acidfree_get_child_albums(&$node) {
    // Empty node usually means access was denied to a request for parent (album) node. In this case exit to avoid improperly formed SQL
    if (! $node->nid) {
      return array();
    }

    $children = Array();
    if (!isset($node->nid))
        return $children;
    $query = "SELECT n.nid, child FROM {node} n ".
        "INNER JOIN {acidfree} ON n.nid = {acidfree}.aid ".
        "INNER JOIN {acidfree_hierarchy} ON {acidfree_hierarchy}.child = {acidfree}.aid ".
        "WHERE parent = {$node->nid} AND class='album' ".
        _acidfree_content_sort_clause();
    $query = db_rewrite_sql($query);
    $kids = db_query($query);
    while ($kid = db_fetch_array($kids)) {
        $children[] = acidfree_get_node_by_id($kid['child']);
    }
    return $children;
}

function _acidfree_get_child_album_count(&$node) {
    if (! $node->nid) {
      return 0;
    }
    $query = "SELECT count(n.nid) AS count FROM {node} n ".
        "INNER JOIN {acidfree} ON n.nid = {acidfree}.aid ".
        "INNER JOIN {acidfree_hierarchy} ON {acidfree_hierarchy}.child = {acidfree}.aid ".
        "WHERE parent = {$node->nid} AND class='album' ".
        _acidfree_content_sort_clause();
    $query = db_rewrite_sql($query);
    $count = db_result(db_query($query));
    return $count;
}

function _acidfree_get_parent(&$node) {
    if ($node->class != 'album' && $node->album != false) {
        return acidfree_get_node_by_id($node->album);
    }
    return acidfree_get_node_by_id($node->parent[0]);
}

function &_acidfree_get_ancestors(&$node) {
    //echo "get_ancestors($node->nid)<br>\n";
    if ($node == null) {
        return Array();
    }
    if ($node->nid == acidfree_get_root())
        return Array();
    $parent = _acidfree_get_parent($node);
    $parents = _acidfree_get_ancestors($parent);
    $parents[] = $parent;
    return $parents;
}

function _acidfree_forward(&$node) {
    // do a sanity check first -- strangly enough, view may be called
    // from other pages and this would forward away from them
    $path = _acidfree_get_path();
    if ($path[0] != 'node' || $path[1] != $node->nid)
        return;

    if ($node->class == 'album') {
        if (($offset = _acidfree_get_pager_offset_from_path(ELEMENT_PAGER)) == null)
            return;
        // Album has an offset in it, locate the applicable node and generate a redirect
        $child = _acidfree_get_children($node, 1, ELEMENT_PAGER, true);
        $child = $child[0];
        if (!$child) {
            drupal_not_found();
            //gotto(__LINE__);
            exit(1);
        }
drupal_set_message("REDIRECT: album");
        $p = "pid={$node->nid}";
        $pager = _acidfree_make_pager_query();
        $nid = $child->nid;
    } else {
        // FIXME: this is part of our ugly pager
        $parent = _acidfree_get_parent($node);
        $offset = _acidfree_get_offset_in_parent($parent, $node);
        $p = "pid={$parent->nid}";
        $pager = _acidfree_make_pager_string(array(ELEMENT_PAGER=>"$offset"));
        $nid = $node->nid;

        // Node ID was present so just hack in the other elements so the system can deal with it. No redirect needed
        $_GET[PAGER_STRING] = $pager;
        $_GET['pid'] = $parent->nid;
        return;
    }

    drupal_goto(url("node/$nid", "{$p}&{$pager}"));
}

function _acidfree_get_offset_in_parent(&$parent, &$node) {
    $query = "SELECT COUNT(nid) FROM {node} n ".
        "INNER JOIN {acidfree} ON {acidfree}.aid = n.nid ".
        "INNER JOIN {acidfree_hierarchy} ON child = aid ".
        "WHERE class <> 'album' AND parent=%d ".
        "AND " . _acidfree_filter_clause('n') . " < '" . _acidfree_filter_clause($node) . "'";

    $query = db_rewrite_sql($query);
    $res = db_query($query, $parent->nid);
    if (db_num_rows($res) == 0)
        return 0;

    return db_result($res);
}

function _acidfree_get_pager_offset_from_path($pager_id) {
    $pager = explode(',', $_GET[PAGER_STRING]);
    if (is_numeric($pager[$pager_id]))
        return $pager[$pager_id];
    return null;
}

/**
 * _acidfree_make_pager_string
 *
 * @arg $values
 *    array of values for pager, keyed on pagerid.  Any values in this
 *    will get merged in with the existing pager offsets
 *
 * @return
 *    a pager string in the form a,b,c,d,e
 */
function _acidfree_make_pager_string($values=array()) {
    $pager = explode(',', $_GET[PAGER_STRING]);
    foreach ($values as $key => $val) {
        $pager[$key] = "$val";
    }
    foreach ($pager as $key => $val) {
        if (!is_numeric($val))
            $pager[$key] = "0";
    }
    return implode(',', $pager);
}

/**
 * _acidfree_make_pager_query
 *
 * @arg $values
 *    array of values for pager, keyed on pagerid.  Any values in this
 *    will get merged in with the existing pager offsets
 *
 * @return
 *    a pager string in the form from=a,b,c,d,e
 */
function _acidfree_make_pager_query($values=array()) {
    return PAGER_STRING.'='._acidfree_make_pager_string($values);
}

function _acidfree_get_class_from_path() {
    global $acidfree_types;
    $path = explode('/', $_GET['q']);
    if ($path[0] == 'node' && $path[1] == 'add' && $path[2] == 'acidfree' &&
        in_array($path[3], array_keys($acidfree_types)))
        return $path[3];
    else if ($path[0] == 'node' && $path[1] == 'add' && count($path) == 2)
        return 'media';
    return 'album';
}

function _acidfree_get_path($string=false) {
    $menu = menu_get_menu();
    $path = $menu['items'][menu_get_active_item()]['path'];
    if ($string)
        return $path;
    return explode('/', $path);
}

function _acidfree_get_parent_from_path() {
    $path = _acidfree_get_path();
    if ($path[0] == 'node' && is_numeric($path[1]) && isset($_GET['pid']))
        return $_GET['pid'];
    return false;
}


/* filesystem functions and helpers */
function _acidfree_filemanager_url($file, $abs=true) {
    $file = module_invoke('filemanager', 'get_file_info', $file);
    return module_invoke('filemanager', 'url', $file, (!$file->active), $abs);
}

function _acidfree_get_thumb_url(&$node, $abs=true) {
    if ($node->class == 'album' && $node->thumb == '')
        return _acidfree_get_album_thumb($node);
    return _acidfree_filemanager_url($node->thumb, $abs);
}

function _acidfree_get_small_url(&$node, $abs=true) {
    return _acidfree_filemanager_url($node->small, $abs);
}

function _acidfree_get_large_url($node, $abs=true) {
    if (!$node->large)
        return false;
    return _acidfree_filemanager_url($node->large, $abs);
}

function _acidfree_filemanager_path($file, $abs=false) {
    $file = module_invoke('filemanager', 'get_file_info', $file);
    return module_invoke('filemanager', 'create_path', $file, (!$file->active), $abs);
}

function _acidfree_get_thumb_path(&$node, $abs=false) {
    return _acidfree_filemanager_path($node->thumb, $abs);
}

function _acidfree_get_small_path(&$node, $abs=false) {
    return _acidfree_filemanager_path($node->small, $abs);
}

function _acidfree_get_large_path($node, $abs=false) {
    if (!$node->large)
        return false;
    return _acidfree_filemanager_path($node->large, $abs);
}

function _acidfree_lockfile() {
    static $file;
    if (!$file) {
        $file = file_directory_temp() . DIRECTORY_SEPARATOR.
            'acidfree' . DIRECTORY_SEPARATOR . 'dir.lock';
        // Check for absolute path.
        if (!preg_match('/^([a-z]\:[\\\\|\/])|(\/)/i', $file)) {
            $file = dirname($_SERVER['SCRIPT_FILENAME']). DIRECTORY_SEPARATOR . $file;
        }
    }
    return $file;
}

function _acidfree_locktmp() {
    if (!is_dir(dirname(_acidfree_lockfile())))
        mkdir_rec(dirname(_acidfree_lockfile()));
    $file = fopen(_acidfree_lockfile(), 'w+');
    if (flock($file, LOCK_EX))
        return $file;
    return false;
}

function acidfree_mktmpdir($prefix) {
    $retry = 50;
    while (($fh = _acidfree_locktmp()) == false)
        $retry++;
    if (!$fh)
        return false;
    $fname = tempnam(dirname(_acidfree_lockfile()), $prefix);
    if (!$fname || !unlink($fname) || !mkdir($fname))
        $fname = false;
    fclose($fh);
    return $fname;
}

function acidfree_mktmpfile($prefix='acidfree', $ext='') {
    $retry = 50;
    while (($fh = _acidfree_locktmp()) == false)
        $retry++;
    if (!$fh)
        return false;
    $fname = tempnam(dirname(_acidfree_lockfile()),$prefix);
    if ($ext) {
        while (file_exists($fname.".$ext")) {
            unlink($fname);
            $fname = tempnam(dirname(_acidfree_lockfile()),$prefix);
        }
        touch($fname.".$ext");
        unlink($fname);
        $fname .= ".$ext";
    }
        
    fclose($fh);
    return $fname;
}

function mkdir_rec($dir, $mode=0770) {
    if (version_compare(PHP_VERSION, "5.0.0", ">=")) {
        return mkdir($dir, $mode, true);
    } else {
        if (!is_dir(dirname($dir))) {
            if (!mkdir_rec(dirname($dir), $mode))
                return false;
        }
        mkdir($dir, $mode);
    }
    return true;
}
function rmdir_rec($path) {
    $dir = opendir($path);
    while ($file = readdir($dir)) {
        if ($file == '.' || $file == '..')
            continue;
        $filename = "{$path}/{$file}";
        if (is_dir($filename)) {
            rmdir_rec($filename);
        } else {
            unlink($filename);
        }
    }
    closedir($dir);
    return rmdir($path);
}

function acidfree_title_from_file($filename) {
    $pinfo = pathinfo($filename);
    return preg_replace("/\.{$pinfo['extension']}/", "", $pinfo['basename']);
}

// is_executable was introduced in php 5.0.0 for windows
if (!function_exists('is_executable')) {
function is_executable($filename) {
    return is_file($filename);
}
}

/**
 * Mime stuff -- it is so hard to deal with mime and magic,
 * so I wrote a wrapper that tries to match mimes with files
 * that acidfree knows about.
 */
function _acidfree_file_mime($f) {
    if (ini_get('safe_mode'))
        return 'safe_mode';
    $f = escapeshellarg($f);
    $ret = explode(' ', trim( `file -biN $f` ));
    return $ret[count($ret)-1];
}
function _acidfree_finfo_file($fname) {
    if (!function_exists('finfo_open'))
        return 'no_finfo';
    $res = finfo_open(FILEINFO_MIME); /* return mime type ala mimetype extension */
    $ret = finfo_file($res, $fname);
    finfo_close($res);
    $ret = explode(' ', $ret);
    return $ret[count($ret)-1];
}

/**
 * acidfree_mime
 * @param $file
 *  the file object that contains filepath, filename, filemime, etc.
 * @return
 *  returns the mimetype using fileinfo extenstion, exec'ing file -ib, 
 *  or based on a guess by file extension of known acidfree types
 */
function acidfree_mime(&$file) {
    // all our files should have an extension
    $file_ext = strtolower(pathinfo($file->filename, PATHINFO_EXTENSION));
    if (strlen($file_ext) < 1) {
        return 'file/unknown';
    }
    global $acidfree_types;
    foreach ($acidfree_types as $type) {
        foreach ($type->mime_ext as $mime=>$ext) {
            if ($ext == $file_ext) {
                $first_guess = $mime;
                break;
            }
        }
        if (isset($first_guess))
            break;
    }
    if (!isset($first_guess)) {
        switch ($file_ext) {
        case 'gz':
        case 'tgz':
            $first_guess = 'application/x-gzip';
            break;
        case 'bz2':
            $first_guess = 'application/x-bzip2';
            break;
        case 'zip':
            $first_guess = 'application/x-zip';
            break;
        }
    }
    $finfo_guess = _acidfree_finfo_file($file->filepath);
    $file_guess = _acidfree_file_mime($file->filepath);
    if (strlen($first_guess) > 0 && $first_guess == $finfo_guess) {
        return $first_guess;
    } elseif (strlen($file_guess) > 0 && $file_guess == $first_guess) {
        return $first_guess;
    } elseif (strlen($first_guess) > 0) {
        return $first_guess;
    } elseif (strlen($file_guess) > 0 && $file_guess == $finfo_guess) {
        return $file_guess;
    } elseif (strlen($file_guess) > 0) {
        return $file_guess;
    } elseif (strlen($finfo_guess) > 0) {
        return $finfo_guess;
    }
    return 'file/unknown';
}


function acidfree_filemanager_areas() {
    return array(array('name'=>t('Acidfree'),'description'=>t('Filestore for Acidfree media (images, videos, etc.)'), 'area'=>'acidfree'));
}

function acidfree_filemanager_download($file) {
    // if it is not an acidfree file, allow some other module to test
    if ($file->area != "acidfree")
        return null;

    // Tedious way to get original node id given an fid in file
    $nid = db_result(db_query("SELECT nid FROM {node} ".
            "INNER JOIN {acidfree} ON {node}.nid = {acidfree}.aid ".
            "WHERE {acidfree}.large = %d OR {acidfree}.small = %d ".
            "OR {acidfree}.thumb = %d", $file->fid, $file->fid, $file->fid));
    $node = acidfree_get_node_by_id($nid);

    // allow for temporary files to be viewed (needed for previews)
    if (!$node && $file->working)
        return true;

    if ($node && node_access('view', $node)) {
        // Returning the logical TRUE lets filemanager stream the file
        // optionally send back an array of headers
        return TRUE;
    } else {
        // Issues a permission denied message
        return FALSE;
    }
}

/**
 * acidfree_private_state
 *
 * @param $node
 *   node to get private state of
 * @return
 *   privacy flag per acidfree (may differ from filemanager)
 *   - If global Acidfree private flag is ON the all ndoes are private
 *   - If anonymous user can access file the assume public, else it's private
 */
function acidfree_private_state(&$node) {
/*  A bug (?) in Drupal prevents a uid being passed to node_access from fully
 *  working. Various access checks are done on the current user BEFORE the uid
 *  is processed. To work around that the current user is temporarily set to
 *  the anonymous user (always ID 0).
 */
    global $user;

    // If UID is already anonymous then ignore this section
    if ($user->uid) {
        $user_old = $user;
        $user = user_load(array('uid' => 0));
    }
    if (variable_get('acidfree_private', 0) OR ! node_access('view', $node)) {
        $private = 1;
    }
    else {
        $private = 0;
    }
    // Restore old user if it was saved
    if (isset($user_old)) {
        $user = $user_old;
    }
    return $private;
}

/**
 * acidfree_add_file
 *
 * @param $node
 *   node object that we are adding file on behalf of
 * @param $path
 *   path to the file we are adding
 * @param $filename
 *   actual filename (path may be using temporary filename)
 * @param $mimetype
 *   mimetype of file
 * @param $remove
 *   whether or not to remove the file after adding it to the filestore
 * @return
 *   returns a filemanager file object containing info about the added file
 *   or false on failure
 */
function acidfree_add_file(&$node, $path, $filename, $mimetype = 'application/unknown', $remove = FALSE) {
    // Is the public/private state of the node different than what filemanager thinks it is?
    $private = acidfree_private_state($node);

    // Add file
    return module_invoke('filemanager', 'add_file', 'acidfree', $path, $filename, $mimetype, $remove, $private);
}

/**
 * acidfree_sync_private
 *
 * @param $node
 *   node object to sync filemanager/node_access privacy stuff
 * @return
 *   number of errors encountered (0 = success)
 */
function acidfree_sync_private(&$node) {
    $err = 0;
    // Change privacy, does nothing if privacy is already in proper state
    $private = acidfree_private_state($node);
    // Change filemanager state on each node size. This is very fast if state is already correct
    foreach (Array('large', 'small', 'thumb') as $size) {
        $fid = (is_object($node->$size) ? $node->$size->fid : $node->$size);
        $file = module_invoke('filemanager', 'set_private', $fid, $private);
        if ($file === FALSE) {
            $err++;
        }
    }
    return $err;
}

/*
 * Implementation of hook_exit
 *
 * If there are any nodes that require privacy synchronization do so now.
 *
 * WARNING: Utilities that use node_save outside of a page request MUST call this
 * method when working with Acidfree nodes otherwise the private state of acidfree
 * nodes will not be correctly reflected in filemanager storage.
 *
 */
function acidfree_exit($destination = NULL) {
    global $ACIDFREE_SYNC_PRIVATE;

    // If not yet initialized then initialize it
    if (! isset($ACIDFREE_SYNC_PRIVATE)) {
        $ACIDFREE_SYNC_PRIVATE = array();
    }

    foreach (array_keys($ACIDFREE_SYNC_PRIVATE) as $nid) {
        $node = acidfree_get_node_by_id($nid);
        if ($node) {
            acidfree_sync_private($node);
        }
        // keep list clean
        unset($ACIDFREE_SYNC_PRIVATE[$nid]);
    }
}

/**
 * Implementation of the pathauto_node hook
 *
 * @arg $op
 *    the operation to perform -- currently 'placeholders' or 'values'
 * @arg $node
 *    if called with $op='value', generate the placeholder values for $node
 * @return
 *    returns the placeholder names or values for the given node
 */
function acidfree_pathauto_node($op, $node=null) {
    switch ($op) {
    case 'placeholders':
        return array(t('[acidfreepath]') => t('Concatentated Acidfree album path (minus root album) e.g. trips/seattle.  Try something like \'albums/[acidfreepath]/[title]\' or something'));
    case 'values':
        {
            $empty = array(t('[acidfreepath]') => '');
            if ($node->type != 'acidfree')
                return $empty;
            if (!$node->class)
                $node = acidfree_get_node_by_id($node->nid);
            $path = _acidfree_get_ancestors($node);
            if (count($path) > 0)
                $root = array_shift($path);
            foreach ($path as $k => $v) {
                $cleanpath[$k] = pathauto_cleanstring($v->title);
            }
            if (count($cleanpath))
                return array(t('[acidfreepath]') => implode('/', $cleanpath));
            return $empty;
        }
    }
}

/*
 * Returns SQL ORDER BY clause to use for sorting the album contents. Includes
 * the ORDER BY. Callers need to prefix/suffix with spaces as needed.
 *
 * This code can be expanded in the future to support additional sorting criteria
 */

function _acidfree_content_sort_clause() {
  $order = _acidfree_content_sort_array();

  $sort = array();
  foreach ($order as $k => $v) {
    // Due to a limitation in sort filter the title can only be sorted ascending
    if ($k == 'title') {
      $v = 'ASC';
    }
    $sort[] = "$k $v";
  }

  return "ORDER BY ". implode(', ', $sort);
}


/**
 * _acidfree_filter_clause
 *
 * This method creates either a complex SQL statement or a string based on the
 * data passed. If table names (strings) are passed then an SQL formula is
 * created that if sorted would simulate the normal contents sort order. If a
 * node object is passed (the second parameter is not used) then a string is
 * created from the node data that can be compared to the first form.
 *
 * This is used only when looking for the number of nodes that precede a given
 * node. It is a bit tricky but saves a server side query (which creates at
 * least a RAM table), a sort of that result set and a loop on the client (this
 * script) to locate the proper node. The query presented will be faster than
 * the above for all  but the simplest cases and for those simple cases both
 * methods are blazing fast.
 *
 * Do NOT use this for normal sorting. It is faster to use the results of
 * _acidfree_content_sort_clause() to sort than creating a server side string
 * and sorting on that.
 *
 * Returns a clause that equates to the sort order passed. It does this by
 * dynamically creating a single string which acts the same as the sort clause.
 * If new columns are added to possible sort range this method MUST be changed.
 */



function _acidfree_filter_clause($node_data = 'n', $acidfree_table = '{acidfree}') {
  $order = _acidfree_content_sort_array();

  // If a node is passed the class returned will be a normal string
  if (is_object($node_data)) {
    $object = TRUE;
  }
  else {
    $object = FALSE;
  }


  $filter = array();
  foreach ($order as $k => $v) {
    switch($k) {
      case 'title':
        if ($object) {
          $filter[] = str_pad($node_data->title, 128, ' ');
        }
        else {
          $filter[] = "rpad({$node_data}.title, 128, ' ')";
        }

        break;

      case 'aid':
          if (strtoupper($v) == 'DESC') {
            if ($object) {
              $filter[] = str_pad(9999999999 - $node_data->nid, 10, '0', STR_PAD_LEFT);
            }
            else {
              $filter[] = "LPAD(CONVERT(9999999999 - {$acidfree_table}.aid, CHAR), 10, '0')";
            }
          }
          else {
            if ($object) {
              $filter[] = str_pad($node_data->nid, 10, '0', STR_PAD_LEFT);
            }
            else {
              $filter[] = "LPAD(CONVERT({$acidfree_table}.aid, CHAR), 10, '0')";
            }
          }
          break;

      case 'weight':
          // Normalize weight to a positive number. Assume weights will not exceed +/- 5555555555
          if (strtoupper($v) == 'DESC') {
            if ($object) {
              $filter[] = str_pad(5555555555 - $node_data->weight, 10, '0', STR_PAD_LEFT);
            }
            else {
              $filter[] = "LPAD(CONVERT(5555555555 - {$acidfree_table}.weight, CHAR), 10, '0')";
            }
          }
          else {
            if ($object) {
              $filter[] = str_pad(5555555555 + $node_data->weight, 10, '0', STR_PAD_LEFT);
            }
            else {
              $filter[] = "LPAD(CONVERT(5555555555 + {$acidfree_table}.weight, CHAR), 10, '0')";
            }
          }
          break;


      default:
          watchdog("Acidfree", t("Sort column '%k' is not supported, it is being ignored", array('%k' => $k)), WATCHDOG_ERROR);
          break;
    }
  }
  if ($object) {
    return implode('', $filter);
  }
  else {
    return 'CONCAT('. implode(', ', $filter) .')';
  }
}

// TODO: In the future this will lookup settings and allow for more complex sorts
function _acidfree_content_sort_array() {
  // For now order is fixed. Someday this may come from settings
  //  - In PHP the array is maintained in added order
  return array( 'weight' => 'ASC', 'aid' => variable_get('acidfree_order', 'DESC'));
}

function acidfree_pager_creator(&$node) {
    global $acidfree_pid;

    $parent = _acidfree_get_parent($node);
    $nodes = _acidfree_get_children($parent, 1, ELEMENT_PAGER, true);
    // this hack is to force the pager into paging in the parent, not the photo
    // trust me, Luke, this is the only way...
    $q = $_GET['q'];
    $_GET['q'] = "node/{$parent->nid}";

    // Get pager per Drupal
    $pager = theme('pager', NULL, 1, ELEMENT_PAGER);
    $_GET['q'] = $q;

    /* Modify generated pager to avoid having to do redirects. Drupal's pager
       assumes a page contains a page of nodes. In this case the page contains
       only the node. The pager theme could be rewritten but that makes upgrades
       and integration into existing sites even harder. This code is a bit of a
       hack but since Drupal lacks a pager link hook this will suffice for now.
    */
    // Allow the pager regex rewriter to know the parent
    $acidfree_pid = $parent->nid;
    $pager = preg_replace_callback(
      '{(href=[\'"])[^\'"]+'. PAGER_STRING .'=([^&"\']*)}',
      '_acidfree_pager_rewrite',
      $pager);

    $_GET['q'] = $q;
    return $pager;
}

/*
 * Translates the href passed from a pager style link to a direct acidfree node
 * link. The link will include the pid (from acidfree_pid global) and from=
 * sequences. The node will be aliased if one exists.
 *
 * @param $matches
 *  0 = full text
 *  1 = href and the ' or "
 *  2 = page offsets, 0 = album, 1 = element, 2+ unknown
 *
 * @returns
 *  A new complete url pointing to the target node
 */
function _acidfree_pager_rewrite($matches) {
    global $acidfree_pid;
    static $offset_lookup = array();
    static $pid_lookup = array();

    // Get page elements
    $pager = explode('%2C', $matches[2]);
    $offset = $pager[ELEMENT_PAGER];

    $nid = $offset_lookup[$acidfree_pid][$offset];
    if (! $nid) {
        $clauses = "SELECT n.nid FROM {node} n INNER JOIN {acidfree} on n.nid = {acidfree}.aid ". 
            "INNER JOIN {acidfree_hierarchy} ON {acidfree_hierarchy}.child = {acidfree}.aid ".
            "WHERE parent = %d AND class <> 'album' ".
            _acidfree_content_sort_clause() .
            " LIMIT %d,1";
        $query = db_rewrite_sql($clauses);
        $nid = db_result(db_query($query, $acidfree_pid, $offset));
        $offset_lookup[$acidfree_pid][$offset] = $nid;
        $parents = db_result(db_query("SELECT COUNT(parent) FROM {acidfree_hierarchy} WHERE child = %d", $nid));
        if ($parents > 1)
            $pid_lookup[$nid] = true;
    }

    // pid: Remembers the album associated with this content.
    //      Content can appear in multiple albums so this is important
    // from: we are faking the from by manually setting the from in $_GET on view
    if ($pid_lookup[$nid]) {
        $pidstr = "pid=$acidfree_pid";
    }
    return $matches[1] . url("node/$nid", $pidstr);
}

?>
