diff --git a/ajax.php b/ajax.php
--- a/ajax.php
+++ b/ajax.php
@@ -97,7 +97,7 @@ if (isset($_REQUEST['school_registration
     $html = school_registration_html($page, $school, $courses);
     if (empty($html))
       slate_permutate_json_error('School\'s registration information producer returned no data.');
-    slate_permutate_json_success(array('html' => $html));
+    slate_permutate_json_success(is_array($html) ? $html : array('html' => $html));
   }
 
 slate_permutate_json_error('Unrecognized command.');
diff --git a/school.d/calvin.inc b/school.d/calvin.inc
--- a/school.d/calvin.inc
+++ b/school.d/calvin.inc
@@ -32,6 +32,7 @@ function calvin_info()
 	       'example_course_id' => 'CS-232',
 	       'registration_url' => 'https://portal.calvin.edu/Pages/WebAdvisor.aspx?title=Express+Registration&PID=ST-WERG',
 	       'student_address' => 'Knight',
+	       'webadvisor_url' => 'https://resources.calvin.edu/selfservice/WebAdvisor',
 	       );
 }
 
diff --git a/school.d/default.inc b/school.d/default.inc
--- a/school.d/default.inc
+++ b/school.d/default.inc
@@ -74,6 +74,12 @@ function default_registration_html(Page 
       $link_text = $school['name'] . '\'s website';
     }
 
+  $synonyms = array();
+  foreach ($courses as $course)
+    foreach ($course as $course_slot)
+      foreach ($course_slot as $section)
+        $synonyms[] = $section->getSynonym();
+
   $html = ''
     . '  
' . PHP_EOL
     . '    Enter these codes into ' . htmlentities($school['name']) . '\'s online course registration' . PHP_EOL
@@ -81,10 +87,19 @@ function default_registration_html(Page 
     . '    to register for classes:' . PHP_EOL
     . '  
' . PHP_EOL
     . '  ' . PHP_EOL;
-  foreach ($courses as $course)
-    foreach ($course as $course_slot)
-      foreach ($course_slot as $section)
-        $html .= '    - ' . htmlentities($section->getSynonym()) . '
 ' . PHP_EOL;
+  foreach ($synonyms as $synonym)
+    $html .= '    - ' . htmlentities($synonym) . '
 ' . PHP_EOL;
   $html .= '  
' . PHP_EOL;
-  return $html;
+  $ret = array('html' => $html);
+
+  if (!empty($school['webadvisor_url']))
+    {
+      $webadvisor_register_url = 'webadvisor.php?school=' . $school['id'] . '§ions=' . implode(',', $synonyms);
+      $ret['html'] = ''
+	. '  Automatically register
' . PHP_EOL
+	. $ret['html'];
+      $ret['location'] = page::uri_resolve($webadvisor_register_url);
+    }
+
+  return $ret;
 }
diff --git a/scripts/displayTables.js b/scripts/displayTables.js
--- a/scripts/displayTables.js
+++ b/scripts/displayTables.js
@@ -124,7 +124,12 @@ jQuery(document).ready( function()
         var tab_course_data_json = jQuery(tab_course_data_json_selector).text();
         var tab_course_data = eval('(' + tab_course_data_json + ')');
 
-	slate_permutate_load(jQuery('#regDialog-content'), {school_registration_html: true, courses: tab_course_data});
+	  slate_permutate_load(jQuery('#regDialog-content'), {school_registration_html: true, courses: tab_course_data},
+			       function(target, data) {
+				   target.html(data.html);
+				   if (data.location)
+				       document.location.href = data.location;
+			       });
 
         jQuery("#regDialog").dialog('open');
 
diff --git a/scripts/webadvisor_tokenidx.js b/scripts/webadvisor_tokenidx.js
new file mode 100644
--- /dev/null
+++ b/scripts/webadvisor_tokenidx.js
@@ -0,0 +1,70 @@
+/*
+ * Assumes that WebAdvisor_scripts.js for WebAdvisor-2.x is loaded,
+ * displayFormHTML() or something was called and thus
+ * readURLParameters() was called. We attempt to extract TOKENIDX and
+ * asynchronously inform slate_permutate about it. We currently assume
+ * we're on a login form too.
+ */
+
+var slate_permutate_input_login;
+
+(function() {
+		var slate_permutate_onload = function() {
+
+				/*
+				 * Override the login form's submission handler to catch the
+				 * case where we're still trying to load the TOKENIDX or
+				 * something else.
+				 */
+				var inputs = document.getElementsByTagName('input');
+				for (var i = 0; i < inputs.length; i ++)
+				{
+						slate_permutate_input_login = inputs.item(i);
+						if (slate_permutate_input_login.getAttribute('name') == 'SUBMIT2')
+								break;
+				}
+				slate_permutate_input_login.setAttribute('value', 'Discovering TOKENIDX...');
+				slate_permutate_input_login.setAttribute('disabled', 'disabled');
+
+				/*
+				 * Discover the TOKENIDX if it's available.
+				 */
+				if (containsParameter(g_tokenIdx))
+				{
+						var TOKENIDX = getURLParameter(g_tokenIdx);
+						var myscript = document.createElement('script');
+						myscript.setAttribute('type', 'text/javascript');
+						myscript.setAttribute('src', decodeURIComponent(getURLParameter('SP_CALLBACK')) + 'callback=slate_permutate_token_callback&TOKENIDX=' + TOKENIDX);
+						document.getElementsByTagName('head').item(0).appendChild(myscript);
+				}
+				else
+				{
+						alert('Unable to discover TOKENIDX. You must register manually.');
+				}
+		}
+
+		/*
+		 * Register to run after either of getWindowHTML(),
+		 * setWindowHTML(), or displayFormHTML() have been run. These are
+		 * run after onload="", so they are required if we're to wait for
+		 * the DOM to load...
+		 */
+		var funcs = ['getWindowHTML', 'setWindowHTML', 'displayFormHTML'];
+		for (var i = 0; i < funcs.length; i ++)
+		{
+				var func = window[funcs[i]];
+				window[funcs[i]] = function() {
+						func();
+						slate_permutate_onload();
+				};
+		}
+})();
+
+function slate_permutate_token_callback(result)
+{
+		if (result)
+		{
+				slate_permutate_input_login.setAttribute('value', 'LOG IN');
+				slate_permutate_input_login.removeAttribute('disabled');
+		}
+}
diff --git a/webadvisor.php b/webadvisor.php
new file mode 100644
--- /dev/null
+++ b/webadvisor.php
@@ -0,0 +1,237 @@
+.
+ */
+
+require_once('inc/class.page.php');
+
+/*
+ * Handle the scripts/webadvisor_tokenidx.js making a TOKENIDX
+ * callback, storing that TOKENIDX in our SESSION for later use.
+ */
+if (!empty($_GET['TOKENIDX']))
+  {
+    page::session_start();
+
+    $_SESSION['webadvisor_TOKENIDX'] = $_GET['TOKENIDX'];
+
+    $result = 'received ' . $_GET['TOKENIDX'];
+
+    header('Content-Type: text/javascript; charset=utf-8');
+
+    if ($jsonp = !empty($_GET['callback']))
+      echo $_GET['callback'] . '(';
+    echo json_encode($result);
+    if ($jsonp)
+      echo ");\n";
+    exit;
+  }
+
+$page = page::page_create('WebAdvisor');
+$school = $page->get_school();
+
+if (empty($school['webadvisor_url']))
+  {
+    if (!empty($school['registration_url']) && preg_match(',(.*/WebAdvisor),', $school['registration_url'], $matches))
+      $school['webadvisor_url'] = $matches[1];
+    else
+      $school['webadvisor_url'] = $school['url'] . 'WebAdvisor';
+  }
+
+/**
+ * \brief
+ *   Calculate the URI necessary for logging into WebAdvisor.
+ *
+ * \param $school
+ *   The school.
+ * \param $dest
+ *   The URI to visit after the user has logged into WebAdvisor and
+ *   the TOKENIDX has been communicated to $tokenidx_callback.
+ * \param $tokenidx_callback
+ *   A JSONP-compatible callback which must be passed the TOKENIDX
+ *   parameter the WebAdvisor is using. Treat as if is terminated with
+ *   a `?' -- i.e., just append the querystring without the `?' to
+ *   this URI when constructing the callback. To use, for example, in
+ *   JavaScript you may create a DOMElement 'script' with attributes
+ *   type="text/javascript" and
+ *   src="$tokenidx_callback?callback=jsonp_callback&TOKENIDX=". When jsonp_callback gets called, your script knows
+ *   that $dest may be returned to. Don't forget to allow the user to
+ *   log in first. This is normally done by setting SP_CALLBACK GET
+ *   variable to this value inserting the
+ *   scripts/webadvisor_tokenidx.js script into the WebAdvisor login
+ *   page using cross-site-scripting HTML injection such as through
+ *   the ERROR GET parameter.
+ * \return
+ *   Just ensure that $tokenidx_callback gets called; do not return
+ *   except by redirecting to $dest.
+ */
+function webadvisor_login($page, array $school, $dest, $tokenidx_callback)
+{
+  if (strpos($dest, '?') !== FALSE)
+    $dest .= '&';
+  else
+    $dest .= '?';
+  $dest .= 'from_webadvisor';
+
+  $webadvisor_login_func = $school['id'] . '_webadvisor_login';
+  if (function_exists($webadvisor_login_func))
+    $webadvisor_login_func($school, $dest);
+
+  /*
+   * The hack we are using is that somehow TOKENIDX=&SS=LGRQ&URL=
+   * will both initialize the user's browser with a token cookie and
+   * then redirect to URL. Trying to use the proper way of loading the
+   * LGRQ (using TYPE=P&PID=UT-LGRQ&PROCESS=-XUTAUTH01) doesn't work
+   * because it drops and ignores our URL parameter, leaving the user
+   * at the KV site. No other URL I've fiddled with seems to be able
+   * to do this combination of logging in and returning the user to us
+   * or a URI of our choosing. Once the user's browser has been
+   * initialized with a TOKENIDX, loading the page
+   * SS=LGRQ&URL=&ERROR= will preserve the ERROR=
+   * necessary for our XSS and insert it into the login page.
+   *
+   * HOWEVER, if the browser already has a TOKENIDX-related cookie,
+   * then visiting TOKENIDX=&SS=LGRQ&URL= will cause WebAdvisor
+   * to keep redirecting to itself infinitely. Similarly, if the
+   * browser does not yet have a TOKENIDX-related cookie,
+   * SS=LGRQ&URL= will redirect the user to URL without giving
+   * the user a cookie. Thus, our strategy is:
+   *
+   * 1. Send the user to
+   *    SS=LGRQ&URL=&SP_CALLBACK=&ERROR=. In
+   *    this case, the URL will be set to have `from_webadvisor' as a
+   *    GET parameter and ERROR will be set to the appropriate XSS for
+   *    the normal login form. Thus, if the user does not have a
+   *    token, he will be directed here and sent to step #2 to get a
+   *    token. Otherwise, the user will have a jump start (already
+   *    having TOKENIDX cookies) and communicate his token to us while
+   *    logging in.
+   *
+   * 2. If webadvisor.php is called with from_webadvisor, that means
+   *    one of two things. It might mean that webadvisor_tokenidx.js
+   *    was called successfully and we have the webadvisor TOKENIDX
+   *    stored in our session. In that case, the user's browser
+   *    already had a WebAdvisor TOKENIDX before we did #1; also, this
+   *    function won't be called in that case because this function is
+   *    only called if TOKENIDX is unknown. Thus, we don't know the
+   *    TOKENIDX, meaning that we need to request that the WebAdvisor
+   *    installation allocate a TOKENIDX for the user and _then_
+   *    proceed directly to the login page to send us TOKENIDX.
+   */
+
+  $login_form_uri = $school['webadvisor_url'] . '?SS=LGRQ&URL=' . rawurlencode($dest)
+    . '&SP_CALLBACK=' . rawurlencode($tokenidx_callback)
+    . '&ERROR=' . rawurlencode('');
+
+  if (isset($_GET['from_webadvisor']))
+    /*
+     * Case 2, infer that browser needs TOKENIDX cookies _and_ that
+     * the following URI won't cause endless looping
+     * (hopefully). Unfortunately, this process is not reentrant.
+     */
+    redir($school['webadvisor_url'] . '?TOKENIDX=&SS=LGRQ&URL=' . rawurlencode($login_form_uri));
+
+  /*
+   * Case 1, assume that the user has a TOKENIDX cookie _but_ make
+   * provisions ($dest has from_webadvisor in it) for needing to
+   * allocate that cookie.
+   */
+  redir($login_form_uri);
+
+  return array(
+    /* 'preload' => $school['webadvisor_url'] . '?TYPE=P&PID=UT-LGRQ&PROCESS=-XUTAUTH01&URL=', */
+    'uri' => $school['webadvisor_url'] . '?SS=LGRQ&URL=' . rawurlencode($login_form_uri),
+  );
+}
+
+function redir($dest)
+{
+  header('HTTP/1.1 302 Found');
+  header('Location: ' . $dest);
+  header('Content-Type: text/plain; charset=utf-8');
+  echo 'Location: ' . $dest;
+  exit;
+}
+
+/*
+ * If the page load was not a redirection from webadvisor, we must
+ * clear our local cache of TOKENIDX's value. We need to get a new
+ * token because we can't guess what SS= value the ST-WERG form will
+ * take unless if we start with a new TOKENIDX which doesn't have any
+ * SSes yet. Also, the old token may have (very likely) expired
+ * because of the short login timeout.
+ */
+if (!isset($_GET['from_webadvisor']))
+  unset($_SESSION['webadvisor_TOKENIDX']);
+
+if (empty($_SESSION['webadvisor_TOKENIDX']))
+  {
+    /*
+     * Get a token for the ST-WERG form and have the user perform the
+     * WebAdmin-specific login. This can only be done after the login form
+     * has an SS allocated for it.
+     */
+    webadvisor_login($page, $school, page::uri_resolve('webadvisor.php') . '?r=' . rand()
+		     . '§ions=' . rawurlencode(empty($_GET['sections']) ? '' : $_GET['sections'])
+		     . '&school=' . rawurlencode($school['id']),
+		     page::uri_resolve('webadvisor.php?'));
+  }
+
+/*
+ * Use the hopefully-still-valid TOKENIDX to initialize an ST-WERG
+ * (STudent Web[A]dvisor Express ReGistration) form. When that form is
+ * iniailized, assume that it has SS=1 and submit the form. &APP=ST
+ */
+$TOKENIDX = $_SESSION['webadvisor_TOKENIDX'];
+$page->head();
+echo '';
+
+$page->foot();