'Image attachment view', 'page callback' => 'image_attach_view_image', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); $items['admin/settings/image/image_attach'] = array( 'title' => 'Image attach', 'description' => 'Enable image attach for content.', 'page callback' => 'drupal_get_form', 'page arguments' => array('image_attach_admin_settings'), 'access arguments' => array('administer site configuration'), 'type' => MENU_LOCAL_TASK, ); return $items; } /** * Implementation of hook_perm() */ function image_attach_perm() { return array('attach images'); } function image_attach_admin_settings() { $form['image_attach_existing'] = array( '#type' => 'radios', '#title' => t('Attach existing images'), '#default_value' => variable_get('image_attach_existing', 1), '#options' => array( 0 => t('Disabled'), 1 => t('Enabled'), ), '#description' => t('When enabled, will allow existing image nodes to be attached instead of uploading new images.'), ); return system_settings_form($form); } /** * Implementation of hook_block(). */ function image_attach_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list': $blocks[0] = array( 'info' => t('Attached images'), 'weight' => 0, 'visibility' => 1, 'pages' => 'node/*', ); return $blocks; case 'view': if ($delta == 0) { if (arg(0) == 'node' && is_numeric(arg(1))) { $node = node_load(arg(1)); if (isset($node->iids)) { $output['subject'] = t('Attached images'); foreach ($node->iids as $iid) { $image = node_load($iid); if (node_access('view', $image)) { $image_nodes[] = $image; } } $output['content'] = theme('image_attach_attached_images_block', $node->nid, $image_nodes); return $output; } } } break; case 'configure': if ($delta == 0) { $image_sizes = array(); foreach (image_get_sizes() as $key => $size) { $image_sizes[$key] = $size['label']; } $form['image_attach_block_0_size'] = array( '#type' => 'select', '#title' => t('Image size'), '#default_value' => variable_get('image_attach_block_0_size', IMAGE_THUMBNAIL), '#options' => $image_sizes, '#description' => t('This determines the size of the image that appears in the block.'), ); return $form; } break; case 'save': if ($delta == 0) { variable_set('image_attach_block_0_size', $edit['image_attach_block_0_size']); } break; } } /** * Implementation of hook_form_FORM_ID_alter(). * * Add settings to the content type settings form. */ function image_attach_form_node_type_form_alter(&$form, $form_state) { if ($form['#node_type']->type != 'image') { _image_check_settings(); $image_sizes = array(IMAGE_ATTACH_HIDDEN => t('')); foreach (image_get_sizes() as $key => $size) { $image_sizes[$key] = $size['label']; } $form['image_attach'] = array( '#type' => 'fieldset', '#title' => t('Image Attach settings'), '#collapsible' => TRUE, '#collapsed' => TRUE, ); $form['image_attach']['image_attach'] = array( '#type' => 'radios', '#title' => t('Attach images'), '#default_value' => variable_get('image_attach_' . $form['#node_type']->type, 0), '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), '#description' => t('Should this node allows users to upload an image?'), ); $form['image_attach']['image_attach_maximum'] = array( '#type' => 'select', '#title' => t('Maximum number of images'), '#default_value' => variable_get('image_attach_maximum_' . $form['#node_type']->type, 0), '#options' => array(0 => t('Unlimited')) + drupal_map_assoc(range(1, 10)), '#description' => t('The maximum number of images that may be attached to nodes of this type.'), ); $form['image_attach']['image_attach_size_teaser'] = array( '#type' => 'select', '#title' => t('Teaser image size'), '#default_value' => variable_get('image_attach_size_teaser_' . $form['#node_type']->type, IMAGE_THUMBNAIL), '#options' => $image_sizes, '#description' => t("This determines the size of the image that appears when the node is displayed as a teaser. 'Hidden' will not show the image."), ); // Hide the weight if CCK is doing the weight for us. if (!module_exists('content')) { $form['image_attach']['image_attach_weight_teaser'] = array( '#type' => 'weight', '#title' => t('Teaser image weight'), '#default_value' => variable_get('image_attach_weight_teaser_' . $form['#node_type']->type, 0), '#description' => t('This value determines the order of the image when displayed in the teaser.'), ); } $form['image_attach']['image_attach_size_body'] = array( '#type' => 'select', '#title' => t('Full node image size'), '#default_value' => variable_get('image_attach_size_body_' . $form['#node_type']->type, IMAGE_THUMBNAIL), '#options' => $image_sizes, '#description' => t("This determines the size of the image that appears when the full node is displayed. 'Hidden' will not show the image."), ); if (!module_exists('content')) { $form['image_attach']['image_attach_weight_body'] = array( '#type' => 'weight', '#title' => t('Full node image weight'), '#default_value' => variable_get('image_attach_weight_body_' . $form['#node_type']->type, 0), '#description' => t('This value determines the order of the image when displayed in the body.'), ); } if (module_exists('content')) { $link = l(t('Manage fields'), $_GET['q'] . '/fields'); $form['image_attach']['#description'] = t('Since you installed CCK module, you can change the image weight in the !link page.', array('!link' => $link)); } } } /** * Implementation of hook_form_alter(). */ function image_attach_form_alter(&$form, $form_state, $form_id) { // Node edit form. if (isset($form['type']['#value']) && $form['type']['#value'] != 'image') { $type = $form['type']['#value']; // If enabled adjust the form. if ($form_id == $type . '_node_form' && variable_get('image_attach_' . $type, 0)) { $node = $form['#node']; _image_check_settings(); $value = !empty($node->new_image) ? '#value' : '#default_value'; $form['#attributes'] = array("enctype" => "multipart/form-data"); // Add a custom submit handler so we can handle image creation on-the-fly $form['#validate'][] = 'image_attach_validate'; // Add a custom submit handler so we can clean up the selection box items. $form['#submit'][] = 'image_attach_node_form_submit'; // Check permissions and settings $may_attach = user_access('attach images'); $may_attach_existing = variable_get('image_attach_existing', 1); $may_upload = user_access('create images'); $has_existing_images = !empty($node->iids); $maximum_images = variable_get('image_attach_maximum_' . $type, 0); // Display the image attach form only if user can attach images, AND // it is allowed to attach existing images or the user is allowed to create new images if ($may_attach && ($may_attach_existing || $may_upload)) { $form['image_attach'] = array( '#type' => 'fieldset', '#title' => t('Attached images'), '#collapsible' => TRUE, '#collapsed' => empty($node->iids), ); if ($maximum_images) { $form['image_attach']['#description'] = format_plural( $maximum_images, 'You may attach 1 image.', 'You may attach up to @count images.' ); } if ($has_existing_images) { $form['image_attach']['image_thumbnail']['#theme'] = 'image_attach_form_thumbnails'; $form['image_attach']['image_thumbnail']['#tree'] = TRUE; $weight = 0; foreach ($node->iids as $iid) { $image = node_load($iid); $form['image_attach']['image_thumbnail'][$iid] = array( 'id' => array( '#type' => 'hidden', '#value' => $iid, ), 'image' => array( '#type' => 'markup', '#value' => image_display($image, 'thumbnail'), ), 'delete' => array( '#type' => 'checkbox', ), 'title' => array( '#type' => 'markup', '#value' => check_plain($image->title), ), 'weight' => array( '#type' => 'weight', '#title' => t('Weight'), '#delta' => count($node->iids), '#default_value' => isset($form_state['values']['image_thumbnail'][$iid]) ? $form_state['values']['image_thumbnail'][$iid]['weight'] : $weight++, ), ); } } // Only show selection box of image nodes if the user may attach some, // or if there are existings ones that may be removed. if ($may_attach_existing || $has_existing_images) { $form['image_attach']['iids'] = array( '#type' => 'select', $value => empty($node->iids) ? NULL : $node->iids, '#multiple' => TRUE, '#size' => 6, // Title, options and description are set just below. ); // User may attach already existing images: show a selection box containing all images. if ($may_attach_existing) { $form['image_attach']['iids']['#title'] = t('Existing images'); $form['image_attach']['iids']['#options'] = _image_attach_get_image_nodes(); if ($may_upload) { $form['image_attach']['iids']['#description'] = t('Choose existing images if you do not upload a new one.'); } else { $form['image_attach']['iids']['#description'] = t('Choose existing images to attach.'); } } // User may only upload new images: show a selection box containing only attached images. else { $form['image_attach']['iids']['#title'] = t('Attached images'); $form['image_attach']['iids']['#options'] = _image_attach_get_image_nodes($node->iids); $form['image_attach']['iids']['#description'] = t('You can remove a previously attached image by unselecting it.'); } } // User may create images, add upload form elements. if ($may_upload) { $form['image_attach']['image'] = array( '#type' => 'file', '#size' => 40, '#title' => t('Upload image'), ); $form['image_attach']['image_title'] = array( '#type' => 'textfield', '#title' => t('Image title'), $value => '', '#description' => t('The title the image will be shown with. Leave blank to use the filename.'), ); // Provide an additional submit button, which adds an image and redirects // the user to the node edit form. $form['image_attach']['image_attach_multiple'] = array( '#type' => 'submit', '#value' => t('Upload'), '#validate' => array('image_attach_validate'), '#submit' => array('image_attach_image_add_submit'), ); } } } } } /** * Get image weights and return an iid array ordered by weight. * * This private function is called by submit handlers to discover how the user * has ordered their images. * It returns an array with identical keys and values (just like the ordinary * iid array). * * @param $raw_weight_data * An array of iids and weights from the drag-to-reorder table, derived from * either the $form_state object or the $node object. * @param $select_box_array * The array of existing images that the user wants to include. This may be * different to the iids passed in $raw_weight_data. * * @return * A weight-sorted (from lightest to heaviest) array of iids, with identical * keys and (integer) values. */ function _image_attach_set_image_weight($raw_weight_data, $select_box_array) { // Build up an array of iids => weights. $weights = array(); foreach ($select_box_array as $iid) { // Check the delete box has not been ticked. if ($raw_weight_data[$iid]['delete'] !== 1) { // If the image exists in the drag-to-reorder array, get its weight, otherwise, assign it the heaviest weight possible. $weights[$iid] = isset($raw_weight_data[$iid]['weight']) ? $raw_weight_data[$iid]['weight'] : count($select_box_array); } } // Sort the iids array by weight. asort($weights); // Take the keys to make the return array. $items = drupal_map_assoc(array_keys($weights)); return $items; } /** * Save attached image nids and rebuild form. * * This submit function adds the new images and returns to the * node edit form directly afterwards, without creating the new node yet. */ function image_attach_image_add_submit(&$form, &$form_state) { // Rebuild the attached image data. if (isset($form_state['values']['iids'])) { db_query("DELETE FROM {image_attach} WHERE nid = %d", $form['nid']['#value']); // Sort the $form_state['values']['iids'] array by weight. if (count($form_state['values']['image_thumbnail'])) { $form_state['values']['iids'] = _image_attach_set_image_weight($form_state['values']['image_thumbnail'], $form_state['values']['iids']); } if (count($form_state['values']['iids'])) { $weight = 0; foreach ($form_state['values']['iids'] as $iid) { db_query("INSERT INTO {image_attach} (nid, iid, weight) VALUES (%d, %d, %d)", $form['nid']['#value'], $iid, $weight++); } } } // Convert taxonomy format from Preview to Object. if (module_exists('taxonomy') && !empty($form_state['values']['taxonomy'])) { $temp_node = new stdClass(); $temp_node->taxonomy = $form_state['values']['taxonomy']; $form_state['values']['taxonomy'] = taxonomy_preview_terms($temp_node); unset($temp_node); } // Rebuild the node edit form. node_form_submit_build_node($form, $form_state); } /** * Image attach validation handler for node edit form. * * Check that the number of images has not exceeded the maximum. * Capture node form submission and immediately create an image if one has been * uploaded. * Note that the new image nodes are created even on preview. Taking several * attempts may create trash. */ function image_attach_validate(&$form, &$form_state) { // Test for whether a file is being uploaded cribbed from file_save_upload(). $uploading_new_image = isset($_FILES['files']) && $_FILES['files']['name']['image'] && is_uploaded_file($_FILES['files']['tmp_name']['image']); // Validate the number of attached images. Filter out the 'None' with array_filter. if ($maximum_images = variable_get('image_attach_maximum_' . $form['#node']->type, 0)) { if (is_array($form_state['values']['iids'])) { $num_images = count(array_filter($form_state['values']['iids'])); } else { $num_images = 0; } $node_type_label = node_get_types('name', $form['#node']->type); if ($num_images >= $maximum_images && $uploading_new_image) { // This error will be set when attempting to upload a new image. // The number already selected may be equal to the maximum, in which case // the error is just to alert the user that their upload has not been performed, and allow // them to unselect an image and proceed to upload the new one. form_set_error('iids', t('There are @count_images images already attached. A new image cannot be uploaded until one or more attached images are unselected.', array( '@count_images' => format_plural($num_images, '1 image', '@count images'), '@maximum' => $maximum_images, '%type' => $node_type_label, ))); } elseif ($num_images > $maximum_images) { form_set_error('iids', t('You have selected @count_images but the maximum for a %type is @maximum.', array( '@count_images' => format_plural($num_images, '1 image', '@count images'), '@maximum' => $maximum_images, '%type' => $node_type_label, ))); } } // Validate and save the uploaded image, providing that there are no errors set. if (!count(form_get_errors())) { $validators = array( 'file_validate_is_image' => array(), ); if ($file = file_save_upload('image', $validators)) { $image_title = $_POST['image_title'] ? $_POST['image_title'] : basename($file->filepath); // Initialize an image properly. $image = image_create_node_from($file->filepath, $image_title, ''); if ($image && !form_get_errors()) { drupal_set_message(t("Created new image to attach to this node. !image_link", array('!image_link' => l($image_title, 'node/' . $image->nid)))); // Append image nid to array of images. $form_state['values']['iids'][$image->nid] = $image->nid; } } else { // Only raise error if user clicked specific Upload button. if ($uploading_new_image) { form_set_error('image_attach', t('Invalid or missing image file for upload and attach.')); } } } } /** * Extra submit handler for node forms. */ function image_attach_node_form_submit(&$form, &$form_state) { // Clear the 0 key in the iids array that arises from selecting the 'None' // option. We do this here so image_attach_nodeapi() gets clean data. unset($form_state['values']['iids'][0]); } /** * Implementation of hook_nodeapi(). */ function image_attach_nodeapi(&$node, $op, $teaser, $page) { // Make sure that if an image is deleted it is detached from any nodes. if ($node->type == 'image') { switch ($op) { case 'delete': db_query("DELETE FROM {image_attach} WHERE iid = %d", $node->nid); } return; } else if (variable_get('image_attach_' . $node->type, 0) == 0) { return; } switch ($op) { case 'insert': case 'update': db_query("DELETE FROM {image_attach} WHERE nid = %d", $node->nid); if (!empty($node->iids)) { if (!empty($node->image_thumbnail)) { // Form submitted, order iids according to drag-to-reorder table. $node->iids = _image_attach_set_image_weight($node->image_thumbnail, $node->iids); } $weight = 0; foreach ($node->iids as $iid) { db_query("INSERT INTO {image_attach} (nid, iid, weight) VALUES (%d, %d, %d)", $node->nid, $iid, $weight++); } } break; case 'delete': db_query("DELETE FROM {image_attach} WHERE nid = %d", $node->nid); break; case 'load': $res = db_query("SELECT iid FROM {image_attach} WHERE nid = %d ORDER BY weight", $node->nid); $iids = array(); while ($iid = db_fetch_array($res)) { $iids[] = $iid['iid']; } return array('iids' => $iids); // Pass the body and teaser objects to the theme again to add the images. case 'view': if (!empty($node->iids)) { $node->image_attach = _image_attach_node_load_attached($node->iids, $teaser); $teaser_or_body = $teaser ? 'teaser' : 'body'; $img_size = variable_get('image_attach_size_' . $teaser_or_body . '_' . $node->type, IMAGE_THUMBNAIL); // Don't show anything if the attached images are set to hidden. if ($img_size == IMAGE_ATTACH_HIDDEN) { return; } // Set weight, either from CCK or our own settings. Cribbed from signup! if (module_exists('content')) { // Due to a bug in CCK (http://drupal.org/node/363456), we need // to call this function twice to ensure we get the real value. // The bug is present when you first enable the setting to // display in the node instead of a separate tab, or when you // first upgrade to the version that contains this code. content_extra_field_weight($node->type, 'image_attach'); $weight = content_extra_field_weight($node->type, 'image_attach'); } else { $weight = variable_get("image_attach_weight_{$teaser_or_body}_{$node->type}", 0); } $node->content['image_attach'] = array( '#weight' => $weight, '#value' => theme('image_attach_attached_images_node', $node->nid, $node->image_attach, $img_size, $teaser), ); } break; case 'rss item': $ret = array(); if (!empty($node->iids) && $image = node_load($node->iids[0])) { $info = image_get_info(file_create_path($image->images[IMAGE_PREVIEW])); $ret[] = array( 'key' => 'enclosure', 'attributes' => array( 'url' => url("image/view/{$node->iids[0]}/" . IMAGE_PREVIEW, array('absolute' => TRUE)), 'length' => $info['file_size'], 'type' => $info['mime_type'], ), ); } return $ret; } } /** * Helper function for hook_nodeapi: view op. * Loads all the required attached image nodes. * * @param $attached_nids * An array of node ids. * @param $teaser * If true, only load the first of the node ids. * * @return * An numerical array of node objects. */ function _image_attach_node_load_attached($attached_nids, $teaser = FALSE) { if ($teaser) { // For the teaser we only want the first image from $attached_nids. // During normal node viewing the array is zero-keyed but during edit // preview it is keyed by iid. Therefore using array_shift() will return the // first image for either of these situations. $attached_nids = array(array_shift($attached_nids)); } foreach ($attached_nids as $nid) { $nodes[] = node_load($nid); } return $nodes; } /** * Fetch an array of all candidate referenced nodes, for use in presenting the * selection form to the user. * * @param $nids * A list of nids to filter on. If not passed, all image nids are returned. */ function _image_attach_get_image_nodes($nids = array()) { $placeholder = ''; // If $nids was passed, build placeholders to put in the query if (count($nids)) { $placeholder = 'AND n.nid IN (' . implode(', ', array_fill(0, sizeof($nids), '%d')) . ') '; } $rows = array(0 => t('- None -')); $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, n.sticky FROM {node} n WHERE n.status = 1 AND n.type = 'image' " . $placeholder . "ORDER BY n.sticky DESC, n.title ASC"), $nids); while ($node = db_fetch_object($result)) { $rows[$node->nid] = $node->title; } return $rows; } /** * Views 2 API handler */ function image_attach_views_api() { return array( 'api' => 2, 'path' => drupal_get_path('module', 'image_attach'), ); } /** * Implementation of hook_theme() registry. */ function image_attach_theme() { return array( 'image_attach_attached_images' => array( 'arguments' => array( 'nid' => 0, 'image_nodes' => array(), 'options' => array(), ), ), 'image_attach_attached_images_node' => array( 'arguments' => array( 'node' => new stdClass(), 'image_nodes' => array(), 'img_size' => IMAGE_THUMBNAIL, 'teaser' => FALSE, ), ), 'image_attach_attached_images_block' => array( 'arguments' => array( 'nid' => 0, 'image_nodes' => array(), ), ), 'image_attach_form_thumbnails' => array( 'arguments' => array('form' => NULL), ), ); } /** * Theme image thumbnails in editing ui to a drag-to-reorder table * * @param $form * The $form object */ function theme_image_attach_form_thumbnails($form) { drupal_add_tabledrag('image-attach-thumbnails-draggable-table', 'order', 'sibling', 'weight'); $header = array(t('Attached images'), t('Remove'), t('Title'), t('Weight')); foreach (element_children($form) as $key) { $element = &$form[$key]; $element['weight']['#attributes']['class'] = 'weight'; $row = array(); $row[] = drupal_render($element['image']); $row[] = drupal_render($element['delete']); $row[] = drupal_render($element['title']); $row[] = drupal_render($element['weight']) . drupal_render($element['id']); $rows[] = array('data' => $row, 'class' => 'draggable'); } $output = theme('table', $header, $rows, array('id' => 'image-attach-thumbnails-draggable-table')); $output .= drupal_render($form); return $output; } /** * Theme attached images shown in nodes. * * @param $nid * The attaching node's id. * @param $image_nodes * The node objects of the images to theme. * @param $img_size * The size at which to show images. * @param $teaser * Whether the $node is being shown as a teaser or not. * * Override this in template.php to include a case statement if you want * different node types to appear differently. * If you have additional image sizes you defined in image.module, you can * use them by theming this function as well. */ function theme_image_attach_attached_images_node($nid, $image_nodes, $img_size, $teaser = FALSE) { drupal_add_css(drupal_get_path('module', 'image_attach') . '/image_attach.css'); $options = array( 'size' => $img_size, 'link' => $teaser ? 'node' : 'image', 'attributes' => array( 'class' => 'image-attach-' . ($teaser ? 'teaser' : 'body'), ), ); // We take the images in reverse order because they are floated to the right, // and we want the apparent left to right order to be correct. $output = theme('image_attach_attached_images', $nid, array_reverse($image_nodes), $options); // Wrap output of potentially multiple images in a DIV. if ($output && !$teaser) { $output = '
' . $output . '
'; } return $output; } /** * Theme the attached images block. * * @param $nid * The attaching node's id. * @param $image_nodes * The attached image nodes. */ function theme_image_attach_attached_images_block($nid, $image_nodes = array()) { // Only return block content if there are images. if (is_array($image_nodes) && count($image_nodes)) { $options = array( 'size' => variable_get('image_attach_block_0_size', IMAGE_THUMBNAIL), 'link' => 'image', ); $output = theme('image_attach_attached_images', $nid, $image_nodes, $options); $output = '
' . $output . '
'; return $output; } } /** * Generic theme function for any set of attached images. * * @param $nid * The id of the attaching node. * @param $image_nodes * The fully loaded image nodes to theme. These do not need to be checked for * access: that happens in this function. * @param $options * An associative array of options, with the following keys: * - 'size' (default IMAGE_THUMBNAIL) * The name of the image derivative size at which to show the images, * eg 'thumbnail'. * - 'link' (default 'image') * Whether and where the images should be linked. This should be one of: * - 'image': link to the image node. Default. * - 'node': link to the attaching node. * - 'none': no link. * - 'attributes' * Extra attributes for the div around each image. */ function theme_image_attach_attached_images($nid, $image_nodes = array(), $options = array()) { // Merge in defaults. $options += array( 'size' => IMAGE_THUMBNAIL, 'link' => 'image', 'attributes' => array(), ); $img_size = $options['size']; $link_destination = $options['link']; // Link images to the attaching node. if ($link_destination == 'node') { $link_path = "node/$nid"; } $output = ''; foreach ($image_nodes as $image) { if (!node_access('view', $image)) { // If the image is restricted, don't show it as an attachment. continue; } // Link images to the image node. if ($link_destination == 'image') { $link_path = "node/$image->nid"; } // Get a fresh copy of the attributes for each image node. $div_attributes = $options['attributes']; // Create CSS classes, beginning with those passed in to the function. $classes = array(); if (isset($div_attributes['class'])) { $classes[] = $div_attributes['class']; } // replace with base class in DIV //$classes[] = 'image-attach-' . $teaser_or_body; $classes[] = 'image-attach-node-' . $image->nid; if (!$image->status) { $classes[] = 'image-unpublished'; } $div_attributes['class'] = implode(' ', $classes); // Add the width as inline CSS. $info = image_get_info(file_create_path($image->images[$img_size])); if (!isset($div_attributes['style'])) { $div_attributes['style'] = ''; } $div_attributes['style'] .= 'width: ' . $info['width'] . 'px;'; $output .= ''; $image_img = image_display($image, $img_size); if (isset($link_path)) { $output .= l($image_img, $link_path, array('html' => TRUE)); } else { $output .= $image_img; } $output .= "\n"; } return $output; } /** * Implementation of hook_content_extra_fields(). */ function image_attach_content_extra_fields($type_name) { if (variable_get('image_attach_' . $type_name, 0)) { $extra['image_attach'] = array( 'label' => t('Attached images'), 'description' => t('Image Attach module form.'), 'weight' => 0, ); return $extra; } }