diff --git a/link.css b/link.css index 3e9200fa8f0dd8fee7afb9128911cd4e14e34f9e..7bdec9e18a560a2c7384c1eb377b158c36066d71 100644 --- a/link.css +++ b/link.css @@ -1,4 +1,8 @@ div.link-field-column { float: left; - width: 50%; -} \ No newline at end of file + width: 48%; +} + +div.link-field-column .form-text { + width: 95%; +} diff --git a/link.install b/link.install index 2ca4a1bc8750b410b5e3e17c57893177f0cf2abe..6c21d26ba292d31ca0a8ac4e7650b10c461b359b 100644 --- a/link.install +++ b/link.install @@ -1,10 +1,16 @@ array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''"), - 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => "''"), - 'attributes' => array('type' => 'mediumtext', 'not null' => FALSE), - ); - content_alter_db_field(array(), array(), $field, $columns); - $db_info = content_database_info($field); - if ($field['multiple']) { - $ret[] = update_sql('INSERT INTO {'. $db_info['table'] .'} (vid, delta, nid, '. $field['field_name'] .'_url, '. $field['field_name'] .'_title, '. $field['field_name'] ."_attributes) SELECT vid, delta, nid, field_url, field_title, attributes FROM {node_field_link_data} WHERE field_name = '". $field['field_name'] ."'"); - } - else { - $ret[] = update_sql('UPDATE {'. $db_info['table'] .'} c, {node_field_link_data} l SET c.'. $field['field_name'] .'_url = l.field_url, c.'. $field['field_name'] .'_title = l.field_title, c.'. $field['field_name'] ."_attributes = l.attributes WHERE l.field_name = '". $field['field_name'] ."' AND c.vid = l.vid AND c.nid = l.nid"); - } +/** + * Change the database schema to allow NULL values. + */ +function link_update_6001() { + $ret = array(); + + // Build a list of fields that need updating. + $update_fields = array(); + foreach (content_types_install() as $type_name => $fields) { + foreach ($fields as $field) { + if ($field['type'] == 'link') { + // We only process a given field once. + $update_fields[$field['field_name']] = $field; + } } } - - $ret[] = update_sql('DROP TABLE {node_field_link_data}'); - - db_query('DELETE FROM {cache}'); + + // Update each field's storage to match the current definition. + foreach ($update_fields as $field) { + $db_info = content_database_info($field); + foreach ($db_info['columns'] as $column) { + db_change_field($ret, $db_info['table'], $column['column'], $column['column'], $column); + $ret[] = update_sql("UPDATE {". $db_info['table'] ."} SET ". $column['column'] ." = NULL WHERE ". $column['column'] ." = '' OR ". $column['column'] ." = 'N;'"); + } + } + + // Let CCK re-associate link fields with Link module and activate the fields. + content_associate_fields('link'); + return $ret; } diff --git a/link.module b/link.module index bb1d8fb11c81d518c5f1a4d398ffa6030cf2df0f..17fa97e9c7ec696140eb1078d2e628a0966c0d92 100644 --- a/link.module +++ b/link.module @@ -10,14 +10,22 @@ define('LINK_EXTERNAL', 'external'); define('LINK_INTERNAL', 'internal'); define('LINK_FRONT', 'front'); define('LINK_EMAIL', 'email'); -define('LINK_DOMAINS', 'aero|arpa|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi'); +define('LINK_DOMAINS', 'aero|arpa|asia|biz|com|cat|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel|mobi|local'); + +define('LINK_TARGET_DEFAULT', 'default'); +define('LINK_TARGET_NEW_WINDOW', '_blank'); +define('LINK_TARGET_TOP', '_top'); +define('LINK_TARGET_USER', 'user'); /** * Implementation of hook_field_info(). */ function link_field_info() { return array( - 'link' => array('label' => 'Link'), + 'link' => array( + 'label' => t('Link'), + 'description' => t('Store a title, href, and attributes in the database to assemble a link.'), + ), ); } @@ -36,7 +44,7 @@ function link_field_settings($op, $field) { '#title' => t('Optional URL'), '#default_value' => $field['url'], '#return_value' => 'optional', - '#description' => t('If checked, the URL field is optional and submitting a title alone will be acceptable. If the URL is ommitted, the title will be displayed as plain text.'), + '#description' => t('If checked, the URL field is optional and submitting a title alone will be acceptable. If the URL is omitted, the title will be displayed as plain text.'), ); $title_options = array( @@ -51,7 +59,7 @@ function link_field_settings($op, $field) { '#title' => t('Link Title'), '#default_value' => isset($field['title']) ? $field['title'] : 'optional', '#options' => $title_options, - '#description' => t('If the link title is optional or required, a field will be displayed to the end user. If the link title is static, the link will always use the same title. If token module is installed, the static title value may use any other node field as its value.'), + '#description' => t('If the link title is optional or required, a field will be displayed to the end user. If the link title is static, the link will always use the same title. If token module is installed, the static title value may use any other node field as its value. Static and token-based titles may include most inline XHTML tags such as strong, em, img, span, etc.'), ); $form['title_value'] = array( @@ -75,14 +83,14 @@ function link_field_settings($op, $field) { $form['enable_tokens'] = array( '#type' => 'checkbox', - '#title' => t('Allow tokens'), - '#default_value' => isset($field['enable_tokens']) ? $field['enable_tokens'] : 1, + '#title' => t('Allow user-entered tokens'), + '#default_value' => isset($field['enable_tokens']) ? $field['enable_tokens'] : 1, '#description' => t('Checking will allow users to enter tokens in URLs and Titles on the node edit form. This does not affect the field settings on this page.'), ); } $form['display'] = array( - '#tree' => true, + '#tree' => TRUE, ); $form['display']['url_cutoff'] = array( '#type' => 'textfield', @@ -94,31 +102,34 @@ function link_field_settings($op, $field) { ); $target_options = array( - 'default' => t('Default (no target attribute)'), - '_top' => t('Open link in window root'), - '_blank' => t('Open link in new window'), - 'user' => t('Allow the user to choose'), + LINK_TARGET_DEFAULT => t('Default (no target attribute)'), + LINK_TARGET_TOP => t('Open link in window root'), + LINK_TARGET_NEW_WINDOW => t('Open link in new window'), + LINK_TARGET_USER => t('Allow the user to choose'), ); $form['attributes'] = array( - '#tree' => true, + '#tree' => TRUE, ); $form['attributes']['target'] = array( '#type' => 'radios', '#title' => t('Link Target'), - '#default_value' => $field['attributes']['target'] ? $field['attributes']['target'] : 'default', + '#default_value' => empty($field['attributes']['target']) ? LINK_TARGET_DEFAULT : $field['attributes']['target'], '#options' => $target_options, ); $form['attributes']['rel'] = array( '#type' => 'textfield', '#title' => t('Rel Attribute'), '#description' => t('When output, this link will have this rel attribute. The most common usage is rel="nofollow" which prevents some search engines from spidering entered links.'), - '#default_value' => $field['attributes']['rel'] ? $field['attributes']['rel'] : '', + '#default_value' => empty($field['attributes']['rel']) ? '' : $field['attributes']['rel'], + '#field_prefix' => 'rel = "', + '#field_suffix' => '"', + '#size' => 20, ); $form['attributes']['class'] = array( '#type' => 'textfield', '#title' => t('Additional CSS Class'), - '#description' => t('When output, this link will have have this class attribute. Multiple classes should be seperated by spaces.'), - '#default_value' => isset($field['attributes']['class']) ? $field['attributes']['class'] : '', + '#description' => t('When output, this link will have have this class attribute. Multiple classes should be separated by spaces.'), + '#default_value' => empty($field['attributes']['class']) ? '' : $field['attributes']['class'], ); return $form; @@ -133,47 +144,14 @@ function link_field_settings($op, $field) { case 'database columns': return array( - 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE), - 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE), + 'url' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE), + 'title' => array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE), 'attributes' => array('type' => 'text', 'size' => 'medium', 'not null' => FALSE), ); - case 'filters': - return array( - 'default' => array( - 'name' => t('URL'), - 'operator' => 'views_handler_operator_like', - 'handler' => 'views_handler_operator_like', - ), - 'title' => array( - 'name' => t('Title'), - 'operator' => 'views_handler_operator_like', - 'handler' => 'views_handler_operator_like', - ), - 'protocol' => array( - 'name' => t('Protocol'), - 'list' => drupal_map_assoc(variable_get('filter_allowed_protocols', array('http', 'https', 'ftp', 'news', 'nntp', 'telnet', 'mailto', 'irc', 'ssh', 'sftp', 'webcal'))), - 'operator' => 'views_handler_operator_or', - 'handler' => 'link_views_protocol_filter_handler', - ), - ); - - case 'arguments': - return array( - 'content: '. $field['field_name'] .'_url' => array( - 'name' => t('Link URL') .': '. t($field['widget']['label']) .' ('. $field['field_name'] .')', - 'handler' => 'link_views_argument_handler', - ), - 'content: '. $field['field_name'] .'_title' => array( - 'name' => t('Link Title') .': '. t($field['widget']['label']) .' ('. $field['field_name'] .')', - 'handler' => 'link_views_argument_handler', - ), - 'content: '. $field['field_name'] .'_target' => array( - 'name' => t('Link Target') .': '. t($field['widget']['label']) .' ('. $field['field_name'] .')', - 'handler' => 'link_views_argument_handler', - ), - ); - + case 'views data': + module_load_include('inc', 'link', 'views/link.views'); + return link_views_content_field_data($field); } } @@ -192,7 +170,7 @@ function theme_link_field_settings($form) { } /** - * Implementation of hook_field_is_empty(). + * Implementation of hook_content_is_empty(). */ function link_content_is_empty($item, $field) { if (empty($item['title']) && empty($item['url'])) { @@ -207,16 +185,12 @@ function link_content_is_empty($item, $field) { function link_field($op, &$node, $field, &$items, $teaser, $page) { switch ($op) { case 'load': - foreach ($items as $delta => $item) { - _link_load($items[$delta], $delta); - } - return $items; - break; + return _link_load($field, $items); case 'validate': $optional_field_found = FALSE; - foreach($items as $delta => $value) { - _link_widget_validate($items[$delta],$delta, $field, $node, $optional_field_found); + foreach ($items as $delta => $value) { + _link_validate($items[$delta], $delta, $field, $node, $optional_field_found); } if ($field['url'] == 'optional' && $field['title'] == 'optional' && $field['required'] && !$optional_field_found) { @@ -224,15 +198,15 @@ function link_field($op, &$node, $field, &$items, $teaser, $page) { } break; - case 'process form values': - foreach($items as $delta => $value) { - _link_widget_process($items[$delta],$delta, $field, $node); + case 'presave': + foreach ($items as $delta => $value) { + _link_process($items[$delta], $delta, $field, $node); } break; case 'sanitize': foreach ($items as $delta => $value) { - link_item_sanitize($items[$delta], $delta, $field, $node); + _link_sanitize($items[$delta], $delta, $field, $node); } break; } @@ -244,7 +218,7 @@ function link_field($op, &$node, $field, &$items, $teaser, $page) { function link_widget_info() { return array( 'link' => array( - 'label' => 'Text Fields for Title and URL', + 'label' => 'Link', 'field types' => array('link'), 'multiple values' => CONTENT_HANDLE_CORE, ), @@ -258,23 +232,32 @@ function link_widget(&$form, &$form_state, $field, $items, $delta = 0) { $element = array( '#type' => $field['widget']['type'], '#default_value' => isset($items[$delta]) ? $items[$delta] : '', + '#title' => $field['widget']['label'], + '#weight' => $field['widget']['weight'], + '#description' => $field['widget']['description'], + '#required' => $field['required'], + '#field' => $field, ); return $element; } - -function _link_load(&$item, $delta = 0) { - // Unserialize the attributes array. - $item['attributes'] = unserialize($item['attributes']); +function _link_load($field, &$items) { + foreach ($items as $delta => $item) { + // Unserialize the attributes array. + $items[$delta]['attributes'] = unserialize($item['attributes']); + } + return array($field['field_name'] => $items); } function _link_process(&$item, $delta = 0, $field, $node) { - // Remove the target attribute if not selected. - if (!$item['attributes']['target'] || $item['attributes']['target'] == "default") { - unset($item['attributes']['target']); - } // Trim whitespace from URL. $item['url'] = trim($item['url']); + + // if no attributes are set then make sure $item['attributes'] is an empty array - this lets $field['attributes'] override it. + if (empty($item['attributes'])) { + $item['attributes'] = array(); + } + // Serialize the attributes array. $item['attributes'] = serialize($item['attributes']); @@ -290,16 +273,16 @@ function _link_validate(&$item, $delta, $field, $node, &$optional_field_found) { if ($item['url'] && !(isset($field['widget']['default_value'][$delta]['url']) && $item['url'] == $field['widget']['default_value'][$delta]['url'] && !$field['required'])) { // Validate the link. if (link_validate_url(trim($item['url'])) == FALSE) { - form_set_error($field['field_name'] .']['. $delta. '][url', t('Not a valid URL.')); + form_set_error($field['field_name'] .']['. $delta .'][url', t('Not a valid URL.')); } // Require a title for the link if necessary. if ($field['title'] == 'required' && strlen(trim($item['title'])) == 0) { - form_set_error($field['field_name'] .']['. $delta. '][title', t('Titles are required for all links.')); + form_set_error($field['field_name'] .']['. $delta .'][title', t('Titles are required for all links.')); } } // Require a link if we have a title. if ($field['url'] !== 'optional' && strlen($item['title']) > 0 && strlen(trim($item['url'])) == 0) { - form_set_error($field['field_name'] .']['. $delta. '][url', t('You cannot enter a title without a link url.')); + form_set_error($field['field_name'] .']['. $delta .'][url', t('You cannot enter a title without a link url.')); } // In a totally bizzaro case, where URLs and titles are optional but the field is required, ensure there is at least one link. if ($field['url'] == 'optional' && $field['title'] == 'optional' && (strlen(trim($item['url'])) != 0 || strlen(trim($item['title'])) != 0)) { @@ -309,7 +292,7 @@ function _link_validate(&$item, $delta, $field, $node, &$optional_field_found) { /** * Cleanup user-entered values for a link field according to field settings. - * + * * @param $item * A single link item, usually containing url, title, and attributes. * @param $delta @@ -320,21 +303,27 @@ function _link_validate(&$item, $delta, $field, $node, &$optional_field_found) { * The node containing this link. */ function _link_sanitize(&$item, $delta, &$field, &$node) { + // Don't try to process empty links. + if (empty($item['url']) && empty($item['title'])) { + return; + } + // Replace URL tokens. if (module_exists('token') && $field['enable_tokens']) { - $node = node_load($node->nid); // Necessary for nodes in views. - $item['url'] = token_replace($item['url'], 'node', $node); + // Load the node if necessary for nodes in views. + $token_node = isset($node->nid) ? node_load($node->nid) : $node; + $item['url'] = token_replace($item['url'], 'node', $token_node); } $type = link_validate_url($item['url']); $url = link_cleanup_url($item['url']); - // Seperate out the anchor if any. + // Separate out the anchor if any. if (strpos($url, '#') !== FALSE) { $item['fragment'] = substr($url, strpos($url, '#') + 1); $url = substr($url, 0, strpos($url, '#')); } - // Seperate out the query string if any. + // Separate out the query string if any. if (strpos($url, '?') !== FALSE) { $item['query'] = substr($url, strpos($url, '?') + 1); $url = substr($url, 0, strpos($url, '?')); @@ -343,9 +332,9 @@ function _link_sanitize(&$item, $delta, &$field, &$node) { $item['url'] = $url; // Create a shortened URL for display. - $display_url = $type == LINK_EMAIL ? str_replace('mailto:', '', $url) : url($url, array('query' => $item['query'], 'fragment' => $item['fragment'], 'absolute' => TRUE)); + $display_url = $type == LINK_EMAIL ? str_replace('mailto:', '', $url) : url($url, array('query' => isset($item['query']) ? $item['query'] : NULL, 'fragment' => isset($item['fragment']) ? $item['fragment'] : NULL, 'absolute' => TRUE)); if ($field['display']['url_cutoff'] && strlen($display_url) > $field['display']['url_cutoff']) { - $display_url = substr($display_url, 0, $field['display']['url_cutoff']) . "..."; + $display_url = substr($display_url, 0, $field['display']['url_cutoff']) ."..."; } $item['display_url'] = $display_url; @@ -359,29 +348,46 @@ function _link_sanitize(&$item, $delta, &$field, &$node) { } // Replace tokens. if (module_exists('token') && ($field['title'] == 'value' || $field['enable_tokens'])) { - $node = node_load($node->nid); // Necessary for nodes in views. - $title = token_replace($title, 'node', $node); + // Load the node if necessary for nodes in views. + $token_node = isset($node->nid) ? node_load($node->nid) : $node; + $title = filter_xss(token_replace($title, 'node', $token_node), array('b', 'br', 'code', 'em', 'i', 'img', 'span', 'strong', 'sub', 'sup', 'tt', 'u')); + $item['html'] = TRUE; } $item['display_title'] = empty($title) ? $item['display_url'] : $title; - // Add attributes defined at the widget level - $attributes = array(); - if (is_array($item['attributes'])) { - foreach($item['attributes'] as $attribute => $attbvalue) { - if (isset($item['attributes'][$attribute]) && $field['attributes'][$attribute] == 'user') { - $attributes[$attribute] = $attbvalue; - } - } - } - // Add attributes defined at the field level - if (is_array($field['attributes'])) { - foreach($field['attributes'] as $attribute => $attbvalue) { - if (!empty($attbvalue) && $attbvalue != 'default' && $attbvalue != 'user') { - $attributes[$attribute] = $attbvalue; - } - } + if (!isset($item['attributes'])) { + $item['attributes'] = array(); + } + + // Unserialize attributtes array if it has not been unserialized yet. + if (!is_array($item['attributes'])) { + $item['attributes'] = (array)unserialize($item['attributes']); + } + + // Add default attributes. + $field['attributes'] += _link_default_attributes(); + + // Merge item attributes with attributes defined at the field level. + $item['attributes'] += $field['attributes']; + + // If user is not allowed to choose target attribute, use default defined at + // field level. + if ($field['attributes']['target'] != LINK_TARGET_USER) { + $item['attributes']['target'] = $field['attributes']['target']; + } + + // Remove the target attribute if the default (no target) is selected. + if (empty($item['attributes']) || $item['attributes']['target'] == LINK_TARGET_DEFAULT) { + unset($item['attributes']['target']); + } + + // Remove the rel=nofollow for internal links. + if ($type != LINK_EXTERNAL && strpos($item['attributes']['rel'], 'nofollow') !== FALSE) { + $item['attributes']['rel'] = str_replace('nofollow', '', $item['attributes']); } - $item['attributes'] = $attributes; + + // Remove empty attributes. + $item['attributes'] = array_filter($item['attributes']); // Add the widget label. $item['label'] = $field['widget']['label']; @@ -401,12 +407,18 @@ function link_theme() { 'link_formatter_plain' => array( 'arguments' => array('element' => NULL), ), + 'link_formatter_url' => array( + 'arguments' => array('element' => NULL), + ), 'link_formatter_short' => array( 'arguments' => array('element' => NULL), ), 'link_formatter_label' => array( 'arguments' => array('element' => NULL), ), + 'link_formatter_separate' => array( + 'arguments' => array('element' => NULL), + ), 'link' => array( 'arguments' => array('element' => NULL), ), @@ -415,60 +427,54 @@ function link_theme() { /** * FAPI theme for an individual text elements. - * - * The textfield or textarea is already rendered by the - * textfield or textarea themes and the html output - * lives in $element['#children']. Override this theme to - * make custom changes to the output. - * - * $element['#field_name'] contains the field name - * $element['#delta'] is the position of this element in the group */ function theme_link($element) { drupal_add_css(drupal_get_path('module', 'link') .'/link.css'); - if ($element['#delta'] == 0) { - + // Prefix single value link fields with the name of the field. + if (empty($element['#field']['multiple'])) { + if (isset($element['url']) && isset($element['title'])) { + $element['url']['#title'] = $element['#title'] .' '. $element['url']['#title']; + $element['title']['#title'] = $element['#title'] .' '. $element['title']['#title']; + } + elseif ($element['url']) { + $element['url']['#title'] = $element['#title']; + } } $output = ''; $output .= '