@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Legalpad - make it work for not logged in users

Summary: Adds "verified" and "secretKey" to Legalpad document signatures. For logged in users using an email address they own, things are verified right away. Otherwise, the email is sent a verification letter. When the user clicks the link the signature is marked verified.

Test Plan: signed the document with a bogus email address not logged in. verified the email that would be sent looked good from command line. followed link and successfully verified bogus email address

Reviewers: epriestley

Reviewed By: epriestley

CC: Korvin, epriestley, aran, asherkin

Maniphest Tasks: T4283

Differential Revision: https://secure.phabricator.com/D7930

+307 -40
+8
resources/sql/autopatches/20140113.legalpadsig.1.sql
··· 1 + ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature 2 + ADD secretKey VARCHAR(20) NOT NULL COLLATE utf8_bin; 3 + 4 + ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature 5 + ADD verified TINYINT(1) DEFAULT 0; 6 + 7 + ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature 8 + ADD KEY `secretKey` (secretKey);
+23
resources/sql/autopatches/20140113.legalpadsig.2.php
··· 1 + <?php 2 + 3 + echo "Adding secretkeys to legalpad document signatures.\n"; 4 + 5 + $table = new LegalpadDocumentSignature(); 6 + $conn_w = $table->establishConnection('w'); 7 + $iterator = new LiskMigrationIterator($table); 8 + foreach ($iterator as $sig) { 9 + $id = $sig->getID(); 10 + 11 + echo "Populating signature {$id}...\n"; 12 + 13 + if (!$sig->getSecretKey()) { 14 + queryfx( 15 + $conn_w, 16 + 'UPDATE %T SET secretKey = %s WHERE id = %d', 17 + $table->getTableName(), 18 + Filesystem::readRandomCharacters(20), 19 + $id); 20 + } 21 + } 22 + 23 + echo "Done.\n";
+2
src/__phutil_library_map__.php
··· 838 838 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 839 839 'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php', 840 840 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', 841 + 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', 841 842 'LegalpadDocumentViewController' => 'applications/legalpad/controller/LegalpadDocumentViewController.php', 842 843 'LegalpadMockMailReceiver' => 'applications/legalpad/mail/LegalpadMockMailReceiver.php', 843 844 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', ··· 3357 3358 'LegalpadDocumentSearchEngine' => 'PhabricatorApplicationSearchEngine', 3358 3359 'LegalpadDocumentSignController' => 'LegalpadController', 3359 3360 'LegalpadDocumentSignature' => 'LegalpadDAO', 3361 + 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', 3360 3362 'LegalpadDocumentViewController' => 'LegalpadController', 3361 3363 'LegalpadMockMailReceiver' => 'PhabricatorObjectMailReceiver', 3362 3364 'LegalpadReplyHandler' => 'PhabricatorMailReplyHandler',
+31
src/applications/auth/query/PhabricatorExternalAccountQuery.php
··· 167 167 return 'PhabricatorApplicationPeople'; 168 168 } 169 169 170 + /** 171 + * Attempts to find an external account and if none exists creates a new 172 + * external account with a shiny new ID and PHID. 173 + * 174 + * NOTE: This function assumes the first item in various query parameters is 175 + * the correct value to use in creating a new external account. 176 + */ 177 + public function loadOneOrCreate() { 178 + $account = $this->executeOne(); 179 + if (!$account) { 180 + $account = new PhabricatorExternalAccount(); 181 + if ($this->accountIDs) { 182 + $account->setAccountID(reset($this->accountIDs)); 183 + } 184 + if ($this->accountTypes) { 185 + $account->setAccountType(reset($this->accountTypes)); 186 + } 187 + if ($this->accountDomains) { 188 + $account->setAccountDomain(reset($this->accountDomains)); 189 + } 190 + if ($this->accountSecrets) { 191 + $account->setAccountSecret(reset($this->accountSecrets)); 192 + } 193 + if ($this->userPHIDs) { 194 + $account->setUserPHID(reset($this->userPHIDs)); 195 + } 196 + $account->save(); 197 + } 198 + return $account; 199 + } 200 + 170 201 }
+2
src/applications/legalpad/application/PhabricatorApplicationLegalpad.php
··· 48 48 'edit/(?P<id>\d+)/' => 'LegalpadDocumentEditController', 49 49 'comment/(?P<id>\d+)/' => 'LegalpadDocumentCommentController', 50 50 'view/(?P<id>\d+)/' => 'LegalpadDocumentViewController', 51 + 'verify/(?P<code>[^/]+)/' => 52 + 'LegalpadDocumentSignatureVerificationController', 51 53 'document/' => array( 52 54 'preview/' => 'PhabricatorMarkupPreviewController'), 53 55 ));
+132 -26
src/applications/legalpad/controller/LegalpadDocumentSignController.php
··· 7 7 8 8 private $id; 9 9 10 + public function shouldRequireLogin() { 11 + return false; 12 + } 13 + 10 14 public function willProcessRequest(array $data) { 11 15 $this->id = $data['id']; 12 16 } ··· 25 29 return new Aphront404Response(); 26 30 } 27 31 28 - $signature = id(new LegalpadDocumentSignature()) 29 - ->loadOneWhere( 30 - 'documentPHID = %s AND documentVersion = %d AND signerPHID = %s', 31 - $document->getPHID(), 32 - $document->getVersions(), 33 - $user->getPHID()); 32 + $signer_phid = null; 33 + $signature = null; 34 + $signature_data = array(); 35 + if ($user->isLoggedIn()) { 36 + $signer_phid = $user->getPHID(); 37 + $signature_data = array( 38 + 'email' => $user->loadPrimaryEmailAddress()); 39 + } else if ($request->isFormPost()) { 40 + $email = new PhutilEmailAddress($request->getStr('email')); 41 + $email_obj = id(new PhabricatorUserEmail()) 42 + ->loadOneWhere('address = %s', $email->getAddress()); 43 + if ($email_obj) { 44 + return $this->signInResponse(); 45 + } 46 + $external_account = id(new PhabricatorExternalAccountQuery()) 47 + ->setViewer($user) 48 + ->withAccountTypes(array('email')) 49 + ->withAccountDomains(array($email->getDomainName())) 50 + ->withAccountIDs(array($email->getAddress())) 51 + ->loadOneOrCreate(); 52 + if ($external_account->getUserPHID()) { 53 + return $this->signInResponse(); 54 + } 55 + $signer_phid = $external_account->getPHID(); 56 + } 57 + 58 + if ($signer_phid) { 59 + $signature = id(new LegalpadDocumentSignature()) 60 + ->loadOneWhere( 61 + 'documentPHID = %s AND documentVersion = %d AND signerPHID = %s', 62 + $document->getPHID(), 63 + $document->getVersions(), 64 + $signer_phid); 65 + } 34 66 35 67 if (!$signature) { 36 68 $has_signed = false; 37 69 $error_view = null; 38 70 $signature = id(new LegalpadDocumentSignature()) 39 - ->setSignerPHID($user->getPHID()) 71 + ->setSignerPHID($signer_phid) 40 72 ->setDocumentPHID($document->getPHID()) 41 - ->setDocumentVersion($document->getVersions()); 42 - $data = array( 43 - 'name' => $user->getRealName(), 44 - 'email' => $user->loadPrimaryEmailAddress()); 45 - $signature->setSignatureData($data); 73 + ->setDocumentVersion($document->getVersions()) 74 + ->setSignatureData($signature_data); 46 75 } else { 47 76 $has_signed = true; 77 + if ($signature->isVerified()) { 78 + $title = pht('Already Signed'); 79 + $body = $this->getVerifiedSignatureBlurb(); 80 + } else { 81 + $title = pht('Already Signed but...'); 82 + $body = $this->getUnverifiedSignatureBlurb(); 83 + } 48 84 $error_view = id(new AphrontErrorView()) 49 85 ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 50 - ->setTitle(pht('Already Signed')) 51 - ->appendChild(pht('Thank you for signing and agreeing')); 52 - $data = $signature->getSignatureData(); 86 + ->setTitle($title) 87 + ->appendChild($body); 88 + $signature_data = $signature->getSignatureData(); 53 89 } 54 90 55 91 $e_name = true; 56 92 $e_email = true; 57 93 $e_address_1 = true; 58 94 $errors = array(); 59 - if ($request->isFormPost()) { 95 + if ($request->isFormPost() && !$has_signed) { 60 96 $name = $request->getStr('name'); 61 97 $email = $request->getStr('email'); 62 98 $address_1 = $request->getStr('address_1'); ··· 68 104 $e_name = pht('Required'); 69 105 $errors[] = pht('Name field is required.'); 70 106 } 71 - $data['name'] = $name; 107 + $signature_data['name'] = $name; 72 108 109 + $addr_obj = null; 73 110 if (!$email) { 74 111 $e_email = pht('Required'); 75 112 $errors[] = pht('Email field is required.'); ··· 81 118 $errors[] = pht('A valid email is required.'); 82 119 } 83 120 } 84 - $data['email'] = $email; 121 + $signature_data['email'] = $email; 85 122 86 123 if (!$address_1) { 87 124 $e_address_1 = pht('Required'); 88 125 $errors[] = pht('Address line 1 field is required.'); 89 126 } 90 - $data['address_1'] = $address_1; 91 - $data['address_2'] = $address_2; 92 - $data['phone'] = $phone; 93 - $signature->setSignatureData($data); 127 + $signature_data['address_1'] = $address_1; 128 + $signature_data['address_2'] = $address_2; 129 + $signature_data['phone'] = $phone; 130 + $signature->setSignatureData($signature_data); 94 131 95 132 if (!$agree) { 96 133 $errors[] = pht( 97 134 'You must check "I agree to the terms laid forth above."'); 98 135 } 136 + 137 + $verified = LegalpadDocumentSignature::UNVERIFIED; 138 + if ($user->isLoggedIn() && $addr_obj) { 139 + $email_obj = id(new PhabricatorUserEmail()) 140 + ->loadOneWhere('address = %s', $addr_obj->getAddress()); 141 + if ($email_obj && $email_obj->getUserPHID() == $user->getPHID()) { 142 + $verified = LegalpadDocumentSignature::VERIFIED; 143 + } 144 + } 145 + $signature->setVerified($verified); 99 146 100 147 if (!$errors) { 101 148 $signature->save(); 102 149 $has_signed = true; 150 + if ($signature->isVerified()) { 151 + $body = $this->getVerifiedSignatureBlurb(); 152 + } else { 153 + $body = $this->getUnverifiedSignatureBlurb(); 154 + $this->sendVerifySignatureEmail( 155 + $document, 156 + $signature); 157 + } 103 158 $error_view = id(new AphrontErrorView()) 104 159 ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 105 - ->setTitle(pht('Signature successful')) 106 - ->appendChild(pht('Thank you for signing and agreeing')); 160 + ->setTitle(pht('Signature Successful')) 161 + ->appendChild($body); 107 162 } else { 108 163 $error_view = id(new AphrontErrorView()) 109 164 ->setTitle(pht('Error in submission.')) ··· 218 273 ->setValue(pht('Sign and Agree')) 219 274 ->setDisabled($has_signed)); 220 275 221 - return id(new PHUIObjectBoxView()) 276 + $view = id(new PHUIObjectBoxView()) 222 277 ->setHeaderText(pht('Sign and Agree')) 223 - ->setErrorView($error_view) 224 278 ->setForm($form); 279 + if ($error_view) { 280 + $view->setErrorView($error_view); 281 + } 282 + return $view; 283 + } 284 + 285 + private function getVerifiedSignatureBlurb() { 286 + return pht('Thank you for signing and agreeing.'); 287 + } 288 + 289 + private function getUnverifiedSignatureBlurb() { 290 + return pht('Thank you for signing and agreeing. However, you must '. 291 + 'verify your email address. Please check your email '. 292 + 'and follow the instructions.'); 293 + } 294 + 295 + private function sendVerifySignatureEmail( 296 + LegalpadDocument $doc, 297 + LegalpadDocumentSignature $signature) { 298 + 299 + $signature_data = $signature->getSignatureData(); 300 + $email = new PhutilEmailAddress($signature_data['email']); 301 + $doc_link = PhabricatorEnv::getProductionURI($doc->getMonogram()); 302 + $path = $this->getApplicationURI(sprintf( 303 + '/verify/%s/', 304 + $signature->getSecretKey())); 305 + $link = PhabricatorEnv::getProductionURI($path); 306 + 307 + $body = <<<EOBODY 308 + Hi {$signature_data['name']}, 309 + 310 + This email address was used to sign a Legalpad document ({$doc_link}). 311 + Please verify you own this email address by clicking this link: 312 + 313 + {$link} 314 + 315 + Your signature is invalid until you verify you own the email. 316 + EOBODY; 317 + 318 + id(new PhabricatorMetaMTAMail()) 319 + ->addRawTos(array($email->getAddress())) 320 + ->setSubject(pht('[Legalpad] Signature Verification')) 321 + ->setBody($body) 322 + ->setRelatedPHID($signature->getDocumentPHID()) 323 + ->saveAndSend(); 324 + } 325 + 326 + private function signInResponse() { 327 + return id(new Aphront403Response()) 328 + ->setForbiddenText(pht( 329 + 'The email address specified is associated with an account. '. 330 + 'Please login to that account and sign this document again.')); 225 331 } 226 332 227 333 }
+84
src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php
··· 1 + <?php 2 + 3 + final class LegalpadDocumentSignatureVerificationController 4 + extends LegalpadController { 5 + 6 + private $code; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->code = $data['code']; 10 + } 11 + 12 + public function shouldRequireEmailVerification() { 13 + return false; 14 + } 15 + 16 + public function shouldRequireLogin() { 17 + return false; 18 + } 19 + 20 + public function processRequest() { 21 + $request = $this->getRequest(); 22 + $user = $request->getUser(); 23 + 24 + $signature = id(new LegalpadDocumentSignature()) 25 + ->loadOneWhere('secretKey = %s', $this->code); 26 + 27 + if (!$signature) { 28 + $title = pht('Unable to Verify Signature'); 29 + $content = pht( 30 + 'The verification code you provided is incorrect or the signature '. 31 + 'has been removed. '. 32 + 'Make sure you followed the link in the email correctly.'); 33 + $uri = $this->getApplicationURI(); 34 + $continue = pht('Rats!'); 35 + } else { 36 + $document = id(new LegalpadDocumentQuery()) 37 + ->setViewer($user) 38 + ->withPHIDs(array($signature->getDocumentPHID())) 39 + ->executeOne(); 40 + // the document could be deleted or have its permissions changed 41 + // 4oh4 time 42 + if (!$document) { 43 + return new Aphront404Response(); 44 + } 45 + $uri = '/'.$document->getMonogram(); 46 + if ($signature->isVerified()) { 47 + $title = pht('Signature Already Verified'); 48 + $content = pht( 49 + 'This signature has already been verified.'); 50 + $continue = pht('Continue to Legalpad Document'); 51 + } else { 52 + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); 53 + $signature 54 + ->setVerified(LegalpadDocumentSignature::VERIFIED) 55 + ->save(); 56 + unset($guard); 57 + $title = pht('Signature Verified'); 58 + $content = pht('The signature is now verified.'); 59 + $continue = pht('Continue to Legalpad Document'); 60 + } 61 + } 62 + 63 + $dialog = id(new AphrontDialogView()) 64 + ->setUser($user) 65 + ->setTitle($title) 66 + ->setMethod('GET') 67 + ->addCancelButton($uri, $continue) 68 + ->appendChild($content); 69 + 70 + $crumbs = $this->buildApplicationCrumbs(); 71 + $crumbs->addTextCrumb(pht('Verify Signature')); 72 + 73 + return $this->buildApplicationPage( 74 + array( 75 + $crumbs, 76 + $dialog, 77 + ), 78 + array( 79 + 'title' => pht('Verify Signature'), 80 + 'device' => true, 81 + )); 82 + } 83 + 84 + }
+4
src/applications/legalpad/storage/LegalpadDocument.php
··· 61 61 return parent::save(); 62 62 } 63 63 64 + public function getMonogram() { 65 + return 'L'.$this->getID(); 66 + } 67 + 64 68 /* -( PhabricatorSubscribableInterface Implementation )-------------------- */ 65 69 66 70 public function isAutomaticallySubscribed($phid) {
+14
src/applications/legalpad/storage/LegalpadDocumentSignature.php
··· 5 5 */ 6 6 final class LegalpadDocumentSignature extends LegalpadDAO { 7 7 8 + const VERIFIED = 0; 9 + const UNVERIFIED = 1; 10 + 8 11 protected $documentPHID; 9 12 protected $documentVersion; 10 13 protected $signerPHID; 11 14 protected $signatureData = array(); 15 + protected $verified; 16 + protected $secretKey; 12 17 13 18 public function getConfiguration() { 14 19 return array( ··· 18 23 ) + parent::getConfiguration(); 19 24 } 20 25 26 + public function save() { 27 + if (!$this->getSecretKey()) { 28 + $this->setSecretKey(Filesystem::readRandomCharacters(20)); 29 + } 30 + return parent::save(); 31 + } 21 32 33 + public function isVerified() { 34 + return $this->getVerified() != self::UNVERIFIED; 35 + } 22 36 23 37 }
+7 -14
src/applications/metamta/receiver/PhabricatorMailReceiver.php
··· 80 80 $email_key = 'phabricator.allow-email-users'; 81 81 $allow_email_users = PhabricatorEnv::getEnvConfig($email_key); 82 82 if ($allow_email_users) { 83 - $xuser = id(new PhabricatorExternalAccount())->loadOneWhere( 84 - 'accountType = %s AND accountDomain = %s and accountID = %s', 85 - 'email', 86 - 'self', 87 - $from); 88 - if (!$xuser) { 89 - $xuser = id(new PhabricatorExternalAccount()) 90 - ->setAccountID($from) 91 - ->setAccountType('email') 92 - ->setAccountDomain('self') 93 - ->setDisplayName($from) 94 - ->setEmail($from) 95 - ->save(); 96 - } 83 + $from_obj = new PhutilEmailAddress($from); 84 + $xuser = id(new PhabricatorExternalAccountQuery()) 85 + ->setViewer($user) 86 + ->withAccountTypes(array('email')) 87 + ->withAccountDomains(array($from_obj->getDomainName(), 'self')) 88 + ->withAccountIDs(array($from_obj->getAddress())) 89 + ->loadOneOrCreate(); 97 90 return $xuser->getPhabricatorUser(); 98 91 } else { 99 92 $reasons[] = pht(