? follow-validation-538172-21.patch
? follow-validation-538172-23.patch
? follow-validation-538172-24.patch
? ttt.sql
Index: follow.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/follow/follow.install,v
retrieving revision 1.3
diff -u -p -r1.3 follow.install
--- follow.install	2 Nov 2009 20:25:53 -0000	1.3
+++ follow.install	3 Nov 2009 22:01:07 -0000
@@ -10,8 +10,7 @@
  * Implementation of hook_install().
  */
 function follow_install() {
-  return;
-  // @todo: Add a default link to this site's node RSS feed.
+  // Add a default link to this site's node RSS feed.
   db_insert('follow_links')
     ->fields(array(
       'name' => 'self',
@@ -48,12 +47,18 @@ function follow_schema() {
         'default' => 0,
         'description' => "User's {users} uid.  Sitewide {follow_links} use uid 0",
       ),
-      'url' => array(
+      'path' => array(
         'type' => 'varchar',
         'length' => 255,
         'not null' => TRUE,
         'default' => '',
-        'description' => 'The url the {follow_links} should point to.',
+        'description' => 'The Drupal path or extenal URL the {follow_links} should point to.',
+      ),
+      'options' => array(
+        'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
+        'type' => 'text',
+        'translatable' => TRUE,
+        'serialize' => TRUE,
       ),
       'weight' => array(
         'type' => 'int',
@@ -65,7 +70,7 @@ function follow_schema() {
     ),
     'primary key' => array('lid'),
     'unique keys' => array(
-      'name_uid' => array('name', 'uid'),
+      'uid_name' => array('uid', 'name'),
     ),
   );
   return $schema;
@@ -80,8 +85,70 @@ function follow_uninstall() {
   variable_del('follow_site_block_user');
 }
 
+/**
+ * Update for caching and schema changes.
+ */
 function follow_update_7000() {
   cache_clear_all('follow:networks', 'cache');
-  return array();
+  $new_schema = array(
+    'path' => array(
+      'type' => 'varchar',
+      'length' => 255,
+      'not null' => TRUE,
+      'default' => '',
+      'description' => 'The Drupal path or extenal URL the {follow_links} should point to.',
+    ),
+    'options' => array(
+      'type' => 'text',
+      'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.',
+    ),
+  );
+  // Since we match against just uid in queries, this field must
+  // come first for the index to be used.
+  db_drop_unique_key('follow_links', 'name_uid');
+  db_add_unique_key('follow_links', 'uid_name', array('uid', 'name'));
+  db_add_field('follow_links', 'path', $new_schema['path']);
+  db_add_field('follow_links', 'options', $new_schema['options']);
+  db_update('follow_links')->fields(array('options' => 'a:0:{}'))->execute();
+  return "
+  db_add_field('follow_links', 'path', \$new_schema['path']);
+  db_add_field('follow_links', 'options', \$new_schema['options']);
+  ";
+}
+
+/**
+ * Update existing data to the new schema.
+ */
+function follow_update_7001(&$sandbox = NULL) {
+  if (!isset($sandbox['max_lid'])) {
+    $sandbox['current_lid'] = 0;
+    $sandbox['max_lid'] = db_query('SELECT MAX(lid) FROM {follow_links}')->fetchField();
+  }
+  $links = db_select('follow_links', 'f')
+    ->fields('f', array('lid', 'url'))
+    ->condition('lid', $sandbox['current_lid'], '>')
+    ->range(0, 100)
+    ->orderBy('lid', 'ASC')
+    ->execute();
+  foreach ($links as $link) {
+    $parsed_url = follow_parse_url($link->url);
+    db_update('follow_links')
+      ->fields(array('path' => $parsed_url['path'], 'options' => serialize($parsed_url['options'])))
+      ->condition('lid', $link->lid)
+      ->execute();
+
+    $sandbox['current_lid'] = $link->lid;
+  }
+
+  $sandbox['#finished'] = empty($sandbox['max_lid']) ? 1 : ($sandbox['current_lid'] / $sandbox['max_lid']);
+}
+
+
+/**
+ * Drop old schema column.
+ */
+function follow_update_7002() {
+  db_drop_field('follow_links', 'url');
+  return "db_drop_field('follow_links', 'url');";
 }
 
Index: follow.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/follow/follow.module,v
retrieving revision 1.6
diff -u -p -r1.6 follow.module
--- follow.module	2 Nov 2009 20:25:53 -0000	1.6
+++ follow.module	3 Nov 2009 22:01:07 -0000
@@ -17,7 +17,8 @@ function follow_help($path, $arg) {
   switch ($path) {
     case 'follow':
     case 'user/%/follow':
-      return t('Please copy and paste urls for each service you would like to display in the block.');
+    case 'admin/config/services/follow':
+      return t('Please copy and paste the url for your public profile or page for each service you would like to display in the block. Links need to match the domain of the service in question.');
     case 'admin/settings/follow':
       return t('Here you can set what the default titles are for the "follow" blocks.  If you would like a custom title, edit the individual blocks <a href="!href">here</a>.', array('!href' => url('admin/build/block')));
   }
@@ -232,7 +233,7 @@ function follow_link_title($uid = 0) {
     if ($setting == FOLLOW_NAME) {
       $account = user_load($uid);
       // Set plain to TRUE for realname module support.
-      return t('Follow !name on', array('!name' => theme('username', $account, array('plain' => TRUE))));
+      return t('Follow !name on', array('!name' => theme('username', array('account' => $account))));
     }
     return t('Follow me on');
   }
@@ -282,7 +283,7 @@ function theme_follow_links($variables) 
   $output = '<div class="follow-links clearfix">';
 
   foreach($links as $link) {
-    $title = $networks[$link->name];
+    $title = $networks[$link->name]['title'];
     $output .= theme('follow_link', array('link' => $link, 'title' => $title));
   }
 
@@ -311,8 +312,8 @@ function theme_follow_link($variables) {
     'class' => implode(' ', $classes),
     'title' => follow_link_title($link->uid) .' '. $title,
   );
-
-  return l($title, $link->url, array('attributes' => $attributes)) . "\n";
+  $link->options['attributes'] = $attributes;
+  return l($title, $link->path, $link->options) . "\n";
 }
 
 /**
@@ -375,16 +376,17 @@ function follow_links_form($form, &$form
   // Put all our existing links at the top, sorted by weight.
   if (is_array($links)) {
     foreach ($links as $name => $link) {
-      $title = $networks[$name];
-      $form['follow_links'][$name] = _follow_links_form_link($link, $title);
+      $title = $networks[$name]['title'];
+      $form['follow_links'][$name] = _follow_links_form_link($link, $title, $uid);
       // Unset this specific network so we don't add the same one again below.
       unset($networks[$name]);
     }
   }
   // Now add all the empty ones.
-  foreach ($networks as $name => $title) {
+  foreach ($networks as $name => $info) {
     $link = new stdClass();
-    $form['follow_links'][$name] = _follow_links_form_link($link, $title);
+    $link->name = $name;
+    $form['follow_links'][$name] = _follow_links_form_link($link, $info['title'], $uid);
   }
 
   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
@@ -395,7 +397,7 @@ function follow_links_form($form, &$form
 /**
  * Helper function to create an individual link form element.
  */
-function _follow_links_form_link($link, $title) {
+function _follow_links_form_link($link, $title, $uid) {
   $elements = array();
 
   $elements['name'] = array(
@@ -413,6 +415,8 @@ function _follow_links_form_link($link, 
   }
   $elements['url'] = array(
     '#type' => 'textfield',
+    '#follow_network' => $link->name,
+    '#follow_uid' => $uid,
     '#default_value' => isset($link->url) ? $link->url : '',
     '#element_validate' => array('follow_url_validate'),
   );
@@ -421,13 +425,99 @@ function _follow_links_form_link($link, 
 }
 
 /**
+ * Like drupal_http_build_query() but without urlencodings.
+ */
+function follow_build_query(array $query, $parent = '') {
+  $params = array();
+
+  foreach ($query as $key => $value) {
+    $key = ($parent ? $parent . '[' . $key . ']' : $key);
+
+    // Recurse into children.
+    if (is_array($value)) {
+      $params[] = follow_build_query($value, $key);
+    }
+    // If a query parameter value is NULL, only append its key.
+    elseif (!isset($value)) {
+      $params[] = $key;
+    }
+    else {
+      $params[] = $key . '=' . $value;
+    }
+  }
+  return implode('&', $params);
+}
+
+/**
+ * Build a url for use in the form.
+ */
+function follow_build_url($path, $options) {
+  $url = $path;
+  if (!empty($options['query'])) {
+    $url .= (strpos($path, '?') !== FALSE ? '&' : '?') . follow_build_query($options['query']);
+  }
+  if (!empty($options['fragment'])) {
+    $url .= '#' . $options['fragment'];
+  }
+  return $url;
+}
+
+/**
+ * Split a Drupal path or external link into path and options like a menu link.
+ */
+function follow_parse_url($url) {
+  $parsed_url = parse_url($url);
+  $defaults = array(
+    'scheme' => '',
+    'host' => '',
+    'port' => '',
+    'path' => '/',
+    'query' => '',
+    'fragment' => '',
+  );
+  $parsed_url += $defaults;
+  $options = array('query' => array(), 'fragment' => $parsed_url['fragment']);
+  // Parse the query string into an array.
+  parse_str($parsed_url['query'], $options['query']);
+  if ($parsed_url['scheme']) {
+    $parsed_url['scheme'] .= '://';
+  }
+  // Throw away port for now.
+  $path = $parsed_url['scheme'] . $parsed_url['host'] . $parsed_url['path'];
+  return array('path' => $path, 'options' => $options);
+}
+
+/**
  * Validates the url field to verify it's actually a url.
  */
 function follow_url_validate($form) {
   $url = trim($form['#value']);
-  if($url && !preg_match("/^[a-zA-Z]+[:\/\/]+[A-Za-z0-9\-_]+\\.+[A-Za-z0-9\.\/%&=\?\-_]+$/i", $url)) {
-    form_error($form, t('The specified url is invalid.'));
+  $networks = follow_networks_load($form['#follow_uid']);
+  $info = $networks[$form['#follow_network']];
+  $regex = follow_build_url_regex($info);
+  $parsed = follow_parse_url($url);
+  if($url && !preg_match($regex, $parsed['path'])) {
+    if (!empty($info['domain'])) {
+      $message = t('The specified url is invalid for the domain %domain.  Make sure you use http://.', array('%domain' => $info['domain']));
+    }
+    else {
+      $message = t('The specified path is invalid.  Please enter a path on this site (e.g. rss.xml or taxonomy/term/1/feed).');
+    }
+    form_error($form, $message);
+  }
+}
+
+/**
+ * Build a regex to validate the url based on a known service url.
+ */
+function follow_build_url_regex($network_info) {
+
+  if (!empty($network_info['domain'])) {
+    // An external link.
+    return '@^https?://([a-z0-9\-_.]+\\.|)' . str_replace('.', '\\.', $network_info['domain']) . '/@i';
   }
+  // An internal link should not have ':'.
+  return '@^[^:]+$@';
 }
 
 /**
@@ -539,6 +629,8 @@ function follow_links_load($uid = 0) {
   $result = db_query($sql, array(':uid' => $uid));
 
   foreach ($result as $link) {
+    $link->options = unserialize($link->options);
+    $link->url = follow_build_url($link->path, $link->options);
     $links[$link->name] = $link;
   }
 
@@ -552,6 +644,10 @@ function follow_links_load($uid = 0) {
  *   A link object to be saved.
  */
 function follow_link_save($link) {
+  $parsed = follow_parse_url($link->url);
+  $link->path = $parsed['path'];
+  $link->options = $parsed['options'];
+
   if (isset($link->lid)) {
     drupal_write_record('follow_links', $link, 'lid');
   }
@@ -608,22 +704,64 @@ function follow_networks_load($uid, $res
  */
 function follow_default_networks($uid) {
   $networks = array(
-    'facebook'  => t('Facebook'),
-    'virb'      => t('Virb'),
-    'myspace'   => t('MySpace'),
-    'twitter'   => t('Twitter'),
-    'picasa'    => t('Picasa'),
-    'flickr'    => t('Flickr'),
-    'youtube'   => t('YouTube'),
-    'vimeo'     => t('Vimeo'),
-    'bliptv'    => t('blip.tv'),
-    'lastfm'    => t('last.fm'),
-    'linkedin'  => t('LinkedIn'),
-    'delicious' => t('Delicious'),
-    'tumblr'    => t('Tumblr'),
+    'facebook'  => array(
+      'title' => t('Facebook'),
+      'domain' => 'facebook.com',
+    ),
+    'virb'      => array(
+      'title' => t('Virb'),
+      'domain' => 'virb.com',
+    ),
+    'myspace'   => array(
+      'title' => t('MySpace'),
+      'domain' => 'myspace.com',
+    ),
+    'twitter'   => array(
+      'title' => t('Twitter'),
+      'domain' => 'twitter.com',
+    ),
+    'picasa'    => array(
+      'title' => t('Picasa'),
+      'domain' => 'picasaweb.google.com',
+    ),
+    'flickr'    => array(
+      'title' => t('Flickr'),
+      'domain' => 'flickr.com',
+    ),
+    'youtube'   => array(
+      'title' => t('YouTube'),
+      'domain' => 'youtube.com',
+    ),
+    'vimeo'     => array(
+      'title' => t('Vimeo'),
+      'domain' => 'vimeo.com',
+    ),
+    'bliptv'    => array(
+      'title' => t('blip.tv'),
+      'domain' => 'blip.tv',
+    ),
+    'lastfm'    => array(
+      'title' => t('last.fm'),
+      'domain' => 'last.fm',
+    ),
+    'linkedin'  => array(
+      'title' => t('LinkedIn'),
+      'domain' => 'linkedin.com',
+    ),
+    'delicious' => array(
+      'title' => t('Delicious'),
+      'domain' => 'delicious.com',
+    ),
+    'tumblr'    => array(
+      'title' => t('Tumblr'),
+      'domain' => 'tumblr.com',
+    ),
   );
   if ($uid == 0) {
-    $networks['self'] = t('This site');
+    $networks['self'] = array(
+      'title' => t('This site (RSS)'),
+      'domain' => '',
+    );
   }
   return $networks;
 }
