? follow-validation-538172-21.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 20:58:45 -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 20:58:45 -0000
@@ -232,7 +232,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 +282,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 +311,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 +375,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 +396,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 +414,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 +424,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.');
+    }
+    form_error($form, $message);
+  }
+}
+
+/**
+ * Build a regex to validate the url based on a known service url.
+ */
+function follow_build_url_regex($network_info) {
+  $regex = '@^';
+  if (!empty($network_info['domain'])) {
+    // An external link.
+    $regex .= 'https?://([a-z0-9\-_.]+\\.|)' . str_replace('.', '\\.', $network_info['domain']) . '/';
   }
+  $regex .= '[A-Za-z0-9./%&=\?\-_]*$@i';
+  return $regex;
 }
 
 /**
@@ -539,6 +628,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 +643,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 +703,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'),
+      'domain' => '',
+    );
   }
   return $networks;
 }
