mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2010-01-04 07:40 pm

[dw-free] crossposter: don't save external passwords

[commit: http://hg.dwscoalition.org/dw-free/rev/3414d4ea320e]

http://bugs.dwscoalition.org/show_bug.cgi?id=959

Allow crossposter to prompt for passwords each post, so we don't save them
to the server.

Patch by [personal profile] allen.

Files modified:
  • bin/upgrading/en.dat
  • cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
  • cgi-bin/weblib.pl
  • htdocs/js/xpost.js
  • htdocs/manage/externalaccount.bml
  • htdocs/manage/externalaccount.bml.text
  • htdocs/tools/endpoints/extacct_auth.bml
--------------------------------------------------------------------------------
diff -r f4777a2dc08a -r 3414d4ea320e bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Mon Jan 04 19:37:22 2010 +0000
+++ b/bin/upgrading/en.dat	Mon Jan 04 19:40:25 2010 +0000
@@ -4747,6 +4747,12 @@ xpost.error.invalidprotocol=Missing or i
 
 xpost.notrespected=This will not respect your default <a [[aopts]]>crosspost settings</a>. Your crossposted entries will not be updated.
 
+xpost.nopw.cancel=Cancel
+
+xpost.nopw.checking=Getting authentication...
+
+xpost.nopw.required=Password required.
+
 xpost.password=Password:
 
 xpost.poll.view=View poll: [[name]]
diff -r f4777a2dc08a -r 3414d4ea320e cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm
--- a/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Mon Jan 04 19:37:22 2010 +0000
+++ b/cgi-bin/DW/External/XPostProtocol/LJXMLRPC.pm	Mon Jan 04 19:40:25 2010 +0000
@@ -75,16 +75,34 @@ sub _call_xmlrpc {
 # LJ-XMLRPC library class.
 sub do_auth {
     my ($self, $xmlrpc, $auth) = @_;
-    
+
+    # if we've already set up an ljsession, just use it.
+    if ($auth->{ljsession}) {
+        return $auth;
+    }
+
     # challenge/response for user validation
     if ($auth->{auth_challenge} && $auth->{auth_response}) {
-        # if we already have them, just return.
-        return {
+        # if we already have a challenge and response, then do a login.
+
+        my $challengecall = $self->_call_xmlrpc($xmlrpc, "sessiongenerate", {
+            ver            => 1,
+            auth_method    => 'challenge',
             username       => $auth->{username},
             auth_challenge => $auth->{auth_challenge},
-            auth_response =>  $auth->{auth_response},
-            success => 1
-        };
+            auth_response  => $auth->{auth_response},
+            expiration     => 'short'
+        });
+
+        if ($challengecall->{success}) {
+            $auth->{success} = 1;
+            $auth->{ljsession} = $challengecall->{result}->{ljsession};
+            return $auth;
+        } else {
+            # just return the result hashref (with error)
+            return $challengecall;
+        }
+
     } else {
         my $challengecall = $self->_call_xmlrpc($xmlrpc, 'getchallenge', {});
         if ($challengecall->{success}) {
@@ -125,14 +143,28 @@ sub call_xmlrpc {
     return $authresp unless $authresp->{success};
 
     # return the results of the call
-    return $self->_call_xmlrpc($xmlrpc, $mode, {
-        ver            => 1,
-        auth_method    => 'challenge',
-        username       => $authresp->{username},
-        auth_challenge => $authresp->{auth_challenge},
-        auth_response  => $authresp->{auth_response},
-        %{ $req || {} }
-    });
+    if ($authresp->{ljsession}) {
+        # do an ljsession login
+        $xmlrpc->transport->http_request->push_header('X-LJ-Auth', 'cookie');
+        $xmlrpc->transport->http_request->push_header( Cookie => "ljsession=" . $authresp->{ljsession} );
+
+        return $self->_call_xmlrpc($xmlrpc, $mode, {
+            ver            => 1,
+            auth_method    => 'cookie',
+            username       => $authresp->{username},
+            %{ $req || {} }
+        });
+    } else {
+        # do a standalone challenge/response login.
+        return $self->_call_xmlrpc($xmlrpc, $mode, {
+            ver            => 1,
+            auth_method    => 'challenge',
+            username       => $authresp->{username},
+            auth_challenge => $authresp->{auth_challenge},
+            auth_response  => $authresp->{auth_response},
+            %{ $req || {} }
+        });
+    }
 }
 
 # does a crosspost using the LJ XML-RPC protocol.  returns a hashref
diff -r f4777a2dc08a -r 3414d4ea320e cgi-bin/weblib.pl
--- a/cgi-bin/weblib.pl	Mon Jan 04 19:37:22 2010 +0000
+++ b/cgi-bin/weblib.pl	Mon Jan 04 19:40:25 2010 +0000
@@ -1274,7 +1274,7 @@ sub entry_form {
                 $out .= "<label for='usejournal' class='left'>" . BML::ml('entryform.postto') . "</label>\n";
                 $out .= LJ::html_select({ 'name' => 'usejournal', 'id' => 'usejournal', 'selected' => $usejournal,
                                     'tabindex' => $tabindex->(), 'class' => 'select',
-                                    "onchange" => "changeSubmit('".$submitprefix."','".$remote->{'user'}."'); getUserTags('$remote->{user}'); changeSecurityOptions('$remote->{user}'); LiveJournal.updateXpostFromJournal('$remote->{user}');" },
+                                    "onchange" => "changeSubmit('".$submitprefix."','".$remote->{'user'}."'); getUserTags('$remote->{user}'); changeSecurityOptions('$remote->{user}'); XPostAccount.updateXpostFromJournal('$remote->{user}');" },
                                     "", $remote->{'user'},
                                     map { $_, $_ } @{$res->{'usejournals'}}) . "\n";
                 $out .= "</p>\n";
@@ -1653,7 +1653,6 @@ MOODS
 
             if ( $remote && ! $altlogin ) {
                 # crosspost
-                $$onload .= " LiveJournal.updateXpostFromJournal('$remote->{user}');";
                 my @accounts = DW::External::Account->get_external_accounts($remote);
                 
                 # populate the per-account html first, so that we only have to 
@@ -1686,37 +1685,44 @@ MOODS
                             'value'    => '1',
                             'selected' => $selected,
                             'tabindex' => $tabindex->(),
-                            'onchange' => 'LiveJournal.xpostAcctUpdated();',
+                            'onchange' => 'XPostAccount.xpostAcctUpdated();',
                                                              }) . "</td>\n";
                         $xpostbydefault = 1 if $selected;
                         
-                        # accounts with no password are disabled for now.
-                        #$accthtml .= "<td>";
-                        #unless ($acct->password) {
+                        $accthtml .= "<td>";
+                        unless ($acct->password) {
                         # password field if no password
-                        #    $accthtml .= "<label for='prop_xpost_password_$acctid'>" . BML::ml('xpost.password') . "</label>";
-                        #    $accthtml .= LJ::html_text({
-                        #        'name' => "prop_xpost_password_$acctid",
-                        #        'id' => "prop_xpost_password_$acctid",
-                        #        'value' => "",
-                        #        'disabled' => 0,
-                        #        'size' => 40,
-                        #        'maxlength' => 80,
-                        #        'type' => 'password'
-                        #    });
-                        #}
-                        #$accthtml .= "</td>\n";
-                        # do a challenge/response if available.
-                        #my $challenge = eval { $acct->challenge; };
-                        #if ($challenge) {
-                        #$accthtml .= "<input type='hidden' name='prop_xpost_chal_$acctid' id='prop_xpost_chal_$acctid' class='xpost_chal' value='$challenge' />";
-                        #$accthtml .= "<input type='hidden' name='prop_xpost_chal_$acctid' id='prop_xpost_chal_$acctid' class='xpost_chal' value='$acctid' />";
-                        #$accthtml .= "<input type='hidden' name='prop_xpost_resp_$acctid' id='prop_xpost_resp_$acctid'/>";
-                        #}
+                            $accthtml .= "<span id='prop_xpost_pwspan_$acctid'>";
+                            $accthtml .= "<label for='prop_xpost_password_$acctid'>" . BML::ml('xpost.password') . "</label>";
+                            $accthtml .= LJ::html_text({
+                                'name' => "prop_xpost_password_$acctid",
+                                'id' => "prop_xpost_password_$acctid",
+                                'value' => "",
+                                'disabled' => 0,
+                                'size' => 40,
+                                'maxlength' => 80,
+                                'type' => 'password',
+                                'class' => 'xpost_pw'
+                            });
+                            $accthtml .= "<span class='xpost_pwstatus' id='prop_xpost_pwstatus_$acctid'></span>";
+                            $accthtml .= "<input type='hidden' name='prop_xpost_chal_$acctid' id='prop_xpost_chal_$acctid' class='xpost_chal' />";
+                            $accthtml .= "<input type='hidden' name='prop_xpost_resp_$acctid' id='prop_xpost_resp_$acctid'/>";
+                            $accthtml .= "</span>";
+                        }
+                        $accthtml .= "</td>\n";
+
                         $accthtml .= "</tr>\n";
                     }
                 }
-
+                $out .= qq [
+                    <script type="text/javascript" language="JavaScript">
+                      // xpost messages
+                      var xpostUser = '$remote->{user}';
+                ];
+                $out .= "var xpostCheckingMessage = '" . BML::ml('xpost.nopw.checking') . "';\n";
+                $out .= "var xpostCancelLabel =  '" . BML::ml('xpost.nopw.cancel') . "';\n";
+                $out .= "var xpostPwRequired = '" . BML::ml('xpost.nopw.required') . "';\n";
+                $out .= "</script>\n";
                 $out .= "<div id='xpostdiv'>\n";
                 $out .= "<p><label for='prop_xpost_check' class='left options'>" . BML::ml('entryform.xpost') . "</label>";
                 $out .= LJ::html_check({
@@ -1728,7 +1734,7 @@ MOODS
                     'selected' => $xpostbydefault,
                     'disabled' => (scalar @accounts) ? '0' : '1',
                     'tabindex' => $xpost_tabindex,
-                    'onchange' => 'LiveJournal.xpostButtonUpdated();',
+                    'onchange' => 'XPostAccount.xpostButtonUpdated();',
                                        });
                 $out .= LJ::help_icon_html('prop_xpost_check');
                 $out .= "<a href = '/manage/settings/?cat=othersites'>" . BML::ml('entryform.xpost.manage') . "</a>";
@@ -1737,11 +1743,7 @@ MOODS
                 $out .= "</table>\n";
                 
                 $out .= "</div>\n";
-                # disable choices if no xpost selected by default.
                 $out .= qq [ 
-              <script type="text/javascript">
-                LiveJournal.xpostAcctUpdated();
-              </script>
               <p class='pkg'>
               <span class='inputgroup-left'></span>
                        ];
@@ -1772,7 +1774,7 @@ PREVIEW
 PREVIEW
             }
             if ($LJ::SPELLER && !$opts->{'disabled_save'}) {
-                $out .= LJ::html_submit('action:spellcheck', BML::ml('entryform.spellcheck'), { 'tabindex' => $tabindex->() }) . "&nbsp;";
+                $out .= LJ::html_submit('action:spellcheck', BML::ml('entryform.spellcheck'), { onclick => 'XPostAccount.doSpellcheck()', tabindex => $tabindex->() }) . "&nbsp;";
             }
             # Update posting date/time
             $out .= "<input type='button' value='" . BML::ml( 'entryform.updatedate' ) . "' onclick='settime(\"" . LJ::ejs( BML::ml( 'entryform.dateupdated' ) ) . "\", this);' tabindex='" . $tabindex->() . "' />";
@@ -1946,7 +1948,7 @@ PREVIEW
 
             # do a double-confirm on delete if we have crossposts that
             # would also get removed
-            my $delete_onclick = "return LiveJournal.confirmDelete('" . LJ::ejs(BML::ml('entryform.delete.confirm')) . "', '" . LJ::ejs(BML::ml('entryform.delete.xposts.confirm')) . "')";
+            my $delete_onclick = "return XPostAccount.confirmDelete('" . LJ::ejs(BML::ml('entryform.delete.confirm')) . "', '" . LJ::ejs(BML::ml('entryform.delete.xposts.confirm')) . "')";
             $out .= LJ::html_submit('action:delete', BML::ml('entryform.delete'), {
                 'disabled' => $opts->{'disabled_delete'},
                 'tabindex' => $tabindex->(),
diff -r f4777a2dc08a -r 3414d4ea320e htdocs/js/xpost.js
--- a/htdocs/js/xpost.js	Mon Jan 04 19:37:22 2010 +0000
+++ b/htdocs/js/xpost.js	Mon Jan 04 19:40:25 2010 +0000
@@ -1,46 +1,167 @@ LiveJournal.xpostButtonUpdated = functio
-LiveJournal.xpostButtonUpdated = function () {
-  var xpost_button = document.getElementById("prop_xpost_check");
-  var xpost_checkboxes = DOM.getElementsByTagAndClassName(document, "input", "xpost_acct_checkbox") || [];
-  for (var i=0; i < xpost_checkboxes.length; i++) {
-    xpost_checkboxes[i].disabled = ! xpost_button.checked;
+// class representing a crosspost account
+XPostAccount = new Class(Object, {
+    init: function (acctid) {
+      this.acctid = acctid;
+      this.checkboxTag = $("prop_xpost_" + acctid);
+      this.chalField = $("prop_xpost_chal_" + acctid);
+      this.statusField = $("prop_xpost_pwstatus_" + acctid);
+      this.respField = $("prop_xpost_resp_" + acctid);
+      this.passField = $("prop_xpost_password_" + acctid);
+      this.pwSpan = $("prop_xpost_pwspan_" + acctid);
+      DOM.addEventListener(this.checkboxTag, 'change', this.checkboxChanged.bindEventListener(this));
+      this.checkboxChanged();
+      this.clearSettings();
+    },
+
+    checkboxChanged: function(evt) {
+      if (this.passField != null) {
+        if (this.checkboxTag.checked) {
+          this.pwSpan.style.display='inline';
+        } else {
+          this.pwSpan.style.display='none';
+        }
+
+      }
+    },
+
+    setDisabled: function(disabled) {
+      this.checkboxTag.disabled = disabled;
+      if (this.passField != null) {
+        this.passField.disabled = disabled;
+      }
+    },
+
+    clearSettings: function () {
+      this.failed = false;
+      this.locked = false;
+    },
+
+    clearPassword: function () {
+      if (this.passField != null) {
+        this.passField.value = "";
+      }
+    },
+
+    doChallengeResponse: function () {
+      // check to see if we need to do a challenge/response for this.
+      if ((! this.locked) && this.chalField != null && this.checkboxTag != null && this.checkboxTag.checked) {
+        this.locked = true;
+
+        if (this.passField == null || this.passField.value == null || this.passField.value == "") {
+          this.setError(xpostPwRequired);
+          this.failed = true;
+          this.locked = false;
+          return;
+        }
+        this.setMessage(xpostCheckingMessage + "<input type='button' onclick='XPostAccount.cancelSubmit()' value='" + xpostCancelLabel + "'/>");
+
+        var opts = {
+          "async": true,
+          "method": "GET",
+          "url": window.parent.Site.siteroot + "/__rpc_extacct_auth?acctid=" + this.acctid,
+          "onError": this.gotError.bind(this),
+          "onData": this.gotInfo.bind(this)
+        };
+        window.parent.HTTPReq.getJSON(opts);
+      }
+    },
+
+    gotError: function (err) {
+      if (this.locked) {
+        this.setError(err);
+        this.failed = true;
+        this.locked = false;
+        XPostAccount.checkComplete();
+      }
+      return;
+    },
+
+    gotInfo: function (data) {
+      if (this.locked) {
+        if (data.error) {
+          this.setError(data.error);
+          this.failed = true;
+          this.locked = false;
+          XPostAccount.checkComplete();
+          return;
+        }
+
+        this.statusField.innerHTML = "";
+        if (!data.success) return;
+
+        var pass = this.passField.value;
+        var res = MD5(data.challenge + MD5(pass));
+        this.respField.value = res;
+        this.chalField.value = data.challenge;
+
+        this.failed = false;
+        this.locked = false;
+        XPostAccount.checkComplete();
+      }
+    },
+
+    setMessage: function(message) {
+      if (this.statusField != null) {
+        this.statusField.innerHTML=message;
+      }
+    },
+
+    setError: function(message) {
+      this.setMessage("<span style='color: red;'>" + message + "</span>");
+    }
+
+});
+
+XPostAccount.xpostButtonUpdated = function () {
+  var xpost_button = $("prop_xpost_check");
+  var allunchecked = true;
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    XPostAccount.accounts[i].setDisabled(! xpost_button.checked);
+    allunchecked = allunchecked && ! XPostAccount.accounts[i].checkboxTag.checked;
+  }
+  if (allunchecked && xpost_button.checked) {
+    for (var i=0; i < XPostAccount.accounts.length; i++) {
+      XPostAccount.accounts[i].checkboxTag.checked = true;
+    }
   }
 }
 
-LiveJournal.xpostAcctUpdated = function () {
-  var xpost_button = document.getElementById("prop_xpost_check");
-  var xpost_checkboxes = DOM.getElementsByTagAndClassName(document, "input", "xpost_acct_checkbox") || [];
+XPostAccount.xpostAcctUpdated = function () {
+  var xpost_button = $("prop_xpost_check");
   var allunchecked = true;
-  for (var i=0; i < xpost_checkboxes.length; i++) {
-    allunchecked = allunchecked && ! xpost_checkboxes[i].checked;
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    allunchecked = allunchecked && ! XPostAccount.accounts[i].checkboxTag.checked;
   }
   xpost_button.checked = ! allunchecked;
   xpost_button.disabled = allunchecked;
 }
 
-LiveJournal.updateXpostFromJournal = function (user) {
+XPostAccount.updateXpostFromJournal = function (user) {
   // only allow crossposts to the user's own journal
   var journal = document.updateForm.usejournal.value;
   var allowXpost = (journal == '' || user == journal);
 
-  var xpost_button = document.getElementById("prop_xpost_check");
+  var xpost_button = $("prop_xpost_check");
   // preserve existing disabled state if xpost allowed
-  xpost_button.disabled = (! allowXpost) || xpost_button.disabled;
-  var xpost_checkboxes = DOM.getElementsByTagAndClassName(document, "input", "xpost_acct_checkbox") || [];
-  for (var i=0; i < xpost_checkboxes.length; i++) {
+  var allunchecked = true;
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    allunchecked = allunchecked && ! XPostAccount.accounts[i].checkboxTag.checked;
+  }
+  xpost_button.disabled = (! allowXpost || allunchecked);
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
     // preserve existing disabled state if xpost allowed
-    xpost_checkboxes[i].disabled = (! allowXpost) || xpost_checkboxes[i].disabled;
+    XPostAccount.accounts[i].setDisabled(! allowXpost || (! xpost_button.checked && ! allunchecked));
   }
 
-  var xpostdiv = document.getElementById('xpostdiv');
+  var xpostdiv = $('xpostdiv');
   if (allowXpost) {
     xpostdiv.style.display = 'block';
   } else {
     xpostdiv.style.display = 'none';
   }
-
 }
 
-LiveJournal.confirmDelete = function (confMessage, xpostConfMessage) {
+XPostAccount.confirmDelete = function (confMessage, xpostConfMessage) {
   // basic confirm
   var conf = confirm(confMessage);
   if (conf) {
@@ -60,73 +181,101 @@ LiveJournal.confirmDelete = function (co
   return conf;
 }
 
-// NOTE:  this functionality is disabled; for now, we're requiring passwords
-// for external accounts.
+XPostAccount.loadAccounts = function () {
+  XPostAccount.skipChecks = false;
+  XPostAccount.accounts = new Array();
+  var xpost_fields = DOM.getElementsByTagAndClassName(document, "input", "xpost_acct_checkbox") || [];
+  for (var i=0;  i < xpost_fields.length; i++) {
+    XPostAccount.accounts[i] = new XPostAccount(xpost_fields[i].id.substring(11));
+  }
+}
 
-// add chal/resp auth to the "login" form if it exists
+// add chal/resp auth to the update form if it exists
 // this requires md5.js
-LiveJournal.setUpXpostForm = function () {
+XPostAccount.setUpXpostForm = function () {
   var updateForm = document.getElementById('updateForm');
-  DOM.addEventListener(updateForm, "submit", LiveJournal.xpostFormSubmitted.bindEventListener(updateForm));
-
+  DOM.addEventListener(updateForm, "submit", XPostAccount.xpostFormSubmitted.bindEventListener(updateForm));
+  XPostAccount.loadAccounts();
+  XPostAccount.xpostAcctUpdated();
+  XPostAccount.updateXpostFromJournal(xpostUser);
 }
 
 // When the form is submitted, compute the challenge response and clear out the plaintext password field
-LiveJournal.xpostFormSubmitted = function (evt) {
+XPostAccount.xpostFormSubmitted = function (evt) {
   var updateForm = evt.target;
+
   if (! updateForm)
     return true;
 
-  var xpost_fields = DOM.getElementsByTagAndClassName(document, "input", "xpost_chal") || [];
-  for (var i=0; i < xpost_fields.length; i++) {
-    var chal_field = xpost_fields[i];
-    if (chal_field.value != null && chal_field.value != "") {
-      var acctid = chal_field.id.substring(16);
+  if (! XPostAccount.skipChecks) {
 
-      var resp_field = document.getElementById("prop_xpost_resp_" + acctid);
-      var pass_field = document.getElementById("prop_xpost_password_" + acctid);
+    $('formsubmit').disabled=true;
+    evt.preventDefault();
 
-      if (chal_field && resp_field && pass_field) {
-        //LiveJournal.getChallenge(username, acctid, resp_field, pass_field);
-      }
+    for (var i = 0; i < XPostAccount.accounts.length; i++) {
+      XPostAccount.accounts[i].doChallengeResponse();
+    }
+
+    XPostAccount.checkComplete();
+  }
+}
+
+XPostAccount.checkComplete = function() {
+  // check to see if all of our accounts are complete
+  for (var i=0; i < XPostAccount.accounts.length; i++) {
+    if (XPostAccount.accounts[i].locked) {
+      return false;
     }
   }
+
+  // all complete; see if there's an error.
+  var acctErr = false;
+  for (var i=0; i < XPostAccount.accounts.length; i++) {
+    if (XPostAccount.accounts[i].failed) {
+      acctErr = true;
+    }
+  }
+
+  if (acctErr) {
+    XPostAccount.doCancel();
+  } else {
+    XPostAccount.doFormSubmit();
+  }
+}
+
+// called when a user manually aborts an auth request attempt.
+XPostAccount.cancelSubmit = function() {
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    XPostAccount.accounts[i].setMessage("");
+  }
+  XPostAccount.doCancel();
+}
+
+// cancels the submit attempt; called either when the user hits cancel,
+// or when one of the auth challenge requests fails
+XPostAccount.doCancel = function() {
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    XPostAccount.accounts[i].clearSettings();
+  }
+  $('formsubmit').disabled=false;
+}
+
+XPostAccount.doSpellcheck = function() {
+  XPostAccount.skipChecks = true;
+}
+
+// does the actual form submit after all ofthe validation and auth requests
+// have taken place
+XPostAccount.doFormSubmit = function() {
+  var updateForm = document.getElementById('updateForm');
+  updateForm.onsubmit = null;
+  // clear out the pw fields.
+  for (var i = 0; i < XPostAccount.accounts.length; i++) {
+    XPostAccount.accounts[i].clearPassword();
+  }
+  updateForm.submit();
+
   return false;
 }
 
-LiveJournal.getChallenge = function(username, acctid, resp_field, pass_field) {
-
-  var url = window.parent.Site.siteroot + "/__rpc_extacct_auth?username=" + username + "&acctid=" + acctid;
-
-  var gotError = function(err) {
-    alert(err+' '+username);
-    return;
-  }
-
-  var gotInfo = function (data) {
-    if (data.error) {
-      alert(data.error + ' ' + username);
-      return;
-    }
-
-    if (!data.success) return;
-
-    var pass = pass_field.value;
-    var res = MD5(data.challenge + MD5(pass));
-    resp_field.value = res;
-    pass_field.value = "";  // dont send clear-text password!
-  }
-
-  var opts = {
-    "async": false,
-    "method": "GET",
-    "url": url,
-    "onError": gotError,
-    "onData": gotInfo
-  };
-
-  window.parent.HTTPReq.getJSON(opts);
-}
-
-// disabled for now.
-//LiveJournal.register_hook("page_load", LiveJournal.setUpXpostForm);
+LiveJournal.register_hook("page_load", XPostAccount.setUpXpostForm);
diff -r f4777a2dc08a -r 3414d4ea320e htdocs/manage/externalaccount.bml
--- a/htdocs/manage/externalaccount.bml	Mon Jan 04 19:37:22 2010 +0000
+++ b/htdocs/manage/externalaccount.bml	Mon Jan 04 19:40:25 2010 +0000
@@ -191,9 +191,22 @@ use strict;
         selected => $editpage ? $editacct->xpostbydefault : $POST{xpostbydefault}
     });
     my $xpostbydefault_errdiv = errdiv(\%errs, "xpostbydefault");
-        $body .= "<br />$xpostbydefault_errdiv" if $xpostbydefault_errdiv;
-        $body .= "</td></tr>\n";
+    $body .= "<br />$xpostbydefault_errdiv" if $xpostbydefault_errdiv;
+    $body .= "</td></tr>\n";
 
+    $body .= "<tr><td class='setting_label'><label for='savepassword'>" . $ML{'.setting.xpost.option.savepassword'} . "</label></td>";
+    $body .= "<td>" . LJ::html_check({
+        name     => "savepassword",
+        value    => 1,
+        id       => "savepassword",
+        selected => LJ::did_post() ? 
+                      $POST{savepassword} :
+                      $editpage ? $editacct->password ne "" : 1,
+    });
+    my $savepassword_errdiv = errdiv(\%errs, "savepassword");
+    $body .= "<br />$savepassword_errdiv" if $savepassword_errdiv;
+    $body .= "</td></tr>\n";
+    
     $body .= "<tr><td>";
     $body .= "<br />";
     $body .= LJ::html_submit(undef, $editpage ? $ML{'.btn.update'} : $ML{'.btn.create'});
@@ -238,6 +251,7 @@ sub create_external_account {
     $opts{password} = $POST->{password};
     $opts{username} = $POST->{username};
     $opts{xpostbydefault} = $POST->{xpostbydefault};
+    $opts{savepassword} = $POST->{savepassword};
 
     # username is required
     if (! $opts{username}) {
@@ -245,12 +259,6 @@ sub create_external_account {
         $ok = 0;
     }
     
-    # password is required, at least for now.
-    if (! $opts{password}) {
-        $errs->{password} = BML::ml('.settings.xpost.error.password.required');
-        $ok = 0;
-    }
-
     # check if it's a default site or a custom site
     if ($POST->{"site"} ne -1) {
         # default site; just use the siteid
@@ -304,6 +312,10 @@ sub create_external_account {
     }
 
     if ($ok) {
+        # if the user requested that we don't save their password, then
+        # don't save their password.
+        $opts{password} = "" if $opts{savepassword};
+
         my $new_acct = DW::External::Account->create($u, \%opts);
         # FIXME add error if create fails.
         if ($new_acct) {
@@ -358,14 +370,11 @@ sub edit_external_account {
     my $acct = DW::External::Account->get_external_account($u, $POST{acctid});
     return 0 unless $acct;
     
-    # password is required, at least for now.
-    if (! $POST{password}) {
-        $errs->{password} = BML::ml('.settings.xpost.error.password.required');
-        return 0;
-    }
-
     my $newpw = $POST{password} || "";
-    if ($POST{password} ne "__passwd__") {
+    if (! $POST{savepassword}) {
+        # don't save the password.
+        $acct->set_password("");
+    } elsif ($POST{password} ne "__passwd__") {
         # we have a password
         $acct->set_password($POST{password});
     }
diff -r f4777a2dc08a -r 3414d4ea320e htdocs/manage/externalaccount.bml.text
--- a/htdocs/manage/externalaccount.bml.text	Mon Jan 04 19:37:22 2010 +0000
+++ b/htdocs/manage/externalaccount.bml.text	Mon Jan 04 19:40:25 2010 +0000
@@ -32,6 +32,8 @@
 
 .setting.xpost.option.password.info=(Please make sure you have caps-lock disabled and enter the correct password.)
 
+.setting.xpost.option.savepassword=Save Password
+
 .setting.xpost.option.servicename=Custom Service Name
 
 .setting.xpost.option.servicetype=Custom Service Type
diff -r f4777a2dc08a -r 3414d4ea320e htdocs/tools/endpoints/extacct_auth.bml
--- a/htdocs/tools/endpoints/extacct_auth.bml	Mon Jan 04 19:37:22 2010 +0000
+++ b/htdocs/tools/endpoints/extacct_auth.bml	Mon Jan 04 19:40:25 2010 +0000
@@ -4,8 +4,6 @@
     use vars qw(%GET);
     use JSON;
     use DW::External::Account;
-
-    # FIXME:  this functionality has not been enabled yet.
 
     # error method
     my $err = sub {
@@ -19,9 +17,6 @@
     BML::set_content_type('text/javascript; charset=utf-8');
     BML::finish();
 
-    # annotate this isn't used yet, just in case
-    return $err->( 'This functionality has not been enabled yet.' );
-    
     # get user
     my $u = LJ::get_remote()
         or return $err->(BML::ml('/tools/endpoints/extacct_auth.bml.error.nouser'));
@@ -36,7 +31,7 @@
 
     # get the auth challenge
     my $challenge = $account->challenge;
-    return $err->BML::ml('/tools/endpoints/extacct_auth.bml.error.nosuchaccount', { account => $account->displayname } ) unless $challenge;
+    return $err->(BML::ml('/tools/endpoints/extacct_auth.bml.error.authfailed', { account => $account->displayname } )) unless $challenge;
 
     # return the challenge
     return LJ::js_dumper( { challenge => $challenge, success => 1 } );
--------------------------------------------------------------------------------