@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

Allow Legalpad document managers to add signature exemptions

Summary:
Ref T5532. Allow document managers to add exemptions, which act like signatures but are tracked a little differently.

The primary use case for us is users who sign a corporate CLA and need a user-level exemption if they don't want to sign an individual CLA.

Test Plan: See screenshots.

Reviewers: btrahan, chad

Reviewed By: chad

Subscribers: epriestley

Maniphest Tasks: T5532

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

+284 -9
+2
resources/sql/autopatches/20140701.legalexemption.1.sql
··· 1 + ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature 2 + ADD isExemption BOOL NOT NULL DEFAULT 0 AFTER verified;
+2
resources/sql/autopatches/20140701.legalexemption.2.sql
··· 1 + ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature 2 + ADD exemptionPHID VARCHAR(64) COLLATE utf8_bin AFTER isExemption;
+4
src/__phutil_library_map__.php
··· 874 874 'LegalpadDocumentSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSearchEngine.php', 875 875 'LegalpadDocumentSignController' => 'applications/legalpad/controller/LegalpadDocumentSignController.php', 876 876 'LegalpadDocumentSignature' => 'applications/legalpad/storage/LegalpadDocumentSignature.php', 877 + 'LegalpadDocumentSignatureAddController' => 'applications/legalpad/controller/LegalpadDocumentSignatureAddController.php', 877 878 'LegalpadDocumentSignatureListController' => 'applications/legalpad/controller/LegalpadDocumentSignatureListController.php', 878 879 'LegalpadDocumentSignatureQuery' => 'applications/legalpad/query/LegalpadDocumentSignatureQuery.php', 879 880 'LegalpadDocumentSignatureSearchEngine' => 'applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php', 880 881 'LegalpadDocumentSignatureVerificationController' => 'applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php', 882 + 'LegalpadDocumentSignatureViewController' => 'applications/legalpad/controller/LegalpadDocumentSignatureViewController.php', 881 883 'LegalpadMockMailReceiver' => 'applications/legalpad/mail/LegalpadMockMailReceiver.php', 882 884 'LegalpadReplyHandler' => 'applications/legalpad/mail/LegalpadReplyHandler.php', 883 885 'LegalpadTransaction' => 'applications/legalpad/storage/LegalpadTransaction.php', ··· 3639 3641 0 => 'LegalpadDAO', 3640 3642 1 => 'PhabricatorPolicyInterface', 3641 3643 ), 3644 + 'LegalpadDocumentSignatureAddController' => 'LegalpadController', 3642 3645 'LegalpadDocumentSignatureListController' => 'LegalpadController', 3643 3646 'LegalpadDocumentSignatureQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3644 3647 'LegalpadDocumentSignatureSearchEngine' => 'PhabricatorApplicationSearchEngine', 3645 3648 'LegalpadDocumentSignatureVerificationController' => 'LegalpadController', 3649 + 'LegalpadDocumentSignatureViewController' => 'LegalpadController', 3646 3650 'LegalpadMockMailReceiver' => 'PhabricatorObjectMailReceiver', 3647 3651 'LegalpadReplyHandler' => 'PhabricatorMailReplyHandler', 3648 3652 'LegalpadTransaction' => 'PhabricatorApplicationTransaction',
+4 -1
src/applications/legalpad/application/PhabricatorApplicationLegalpad.php
··· 54 54 'LegalpadDocumentSignatureVerificationController', 55 55 'signatures/(?:(?P<id>\d+)/)?(?:query/(?P<queryKey>[^/]+)/)?' => 56 56 'LegalpadDocumentSignatureListController', 57 + 'addsignature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureAddController', 58 + 'signature/(?P<id>\d+)/' => 'LegalpadDocumentSignatureViewController', 57 59 'document/' => array( 58 - 'preview/' => 'PhabricatorMarkupPreviewController'), 60 + 'preview/' => 'PhabricatorMarkupPreviewController', 61 + ), 59 62 )); 60 63 } 61 64
+18 -6
src/applications/legalpad/controller/LegalpadDocumentSignController.php
··· 101 101 102 102 // In this case, we know they've signed. 103 103 $signed_at = $signature->getDateCreated(); 104 + 105 + if ($signature->getIsExemption()) { 106 + $exemption_phid = $signature->getExemptionPHID(); 107 + $handles = $this->loadViewerHandles(array($exemption_phid)); 108 + $exemption_handle = $handles[$exemption_phid]; 109 + 110 + $signed_text = pht( 111 + 'You do not need to sign this document. '. 112 + '%s added a signature exemption for you on %s.', 113 + $exemption_handle->renderLink(), 114 + phabricator_datetime($signed_at, $viewer)); 115 + } else { 116 + $signed_text = pht( 117 + 'You signed this document on %s.', 118 + phabricator_datetime($signed_at, $viewer)); 119 + } 120 + 104 121 $signed_status = id(new AphrontErrorView()) 105 122 ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 106 - ->setErrors( 107 - array( 108 - pht( 109 - 'You signed this document on %s.', 110 - phabricator_datetime($signed_at, $viewer)), 111 - )); 123 + ->setErrors(array($signed_text)); 112 124 } 113 125 114 126 $e_name = true;
+127
src/applications/legalpad/controller/LegalpadDocumentSignatureAddController.php
··· 1 + <?php 2 + 3 + final class LegalpadDocumentSignatureAddController extends LegalpadController { 4 + 5 + private $id; 6 + 7 + public function willProcessRequest(array $data) { 8 + $this->id = $data['id']; 9 + } 10 + 11 + public function processRequest() { 12 + $request = $this->getRequest(); 13 + $viewer = $request->getUser(); 14 + 15 + $document = id(new LegalpadDocumentQuery()) 16 + ->setViewer($viewer) 17 + ->needDocumentBodies(true) 18 + ->requireCapabilities( 19 + array( 20 + PhabricatorPolicyCapability::CAN_VIEW, 21 + PhabricatorPolicyCapability::CAN_EDIT, 22 + )) 23 + ->withIDs(array($this->id)) 24 + ->executeOne(); 25 + if (!$document) { 26 + return new Aphront404Response(); 27 + } 28 + 29 + $next_uri = $this->getApplicationURI('signatures/'.$document->getID().'/'); 30 + 31 + $e_user = true; 32 + $v_users = array(); 33 + $v_notes = ''; 34 + $errors = array(); 35 + 36 + if ($request->isFormPost()) { 37 + $v_notes = $request->getStr('notes'); 38 + $v_users = array_slice($request->getArr('users'), 0, 1); 39 + 40 + $user_phid = head($v_users); 41 + if (!$user_phid) { 42 + $e_user = pht('Required'); 43 + $errors[] = pht('You must choose a user to exempt.'); 44 + } else { 45 + $user = id(new PhabricatorPeopleQuery()) 46 + ->setViewer($viewer) 47 + ->withPHIDs(array($user_phid)) 48 + ->executeOne(); 49 + 50 + if (!$user) { 51 + $e_user = pht('Invalid'); 52 + $errors[] = pht('That user does not exist.'); 53 + } else { 54 + $signature = id(new LegalpadDocumentSignatureQuery()) 55 + ->setViewer($viewer) 56 + ->withDocumentPHIDs(array($document->getPHID())) 57 + ->withSignerPHIDs(array($user->getPHID())) 58 + ->executeOne(); 59 + if ($signature) { 60 + $e_user = pht('Signed'); 61 + $errors[] = pht('That user has already signed this document.'); 62 + } else { 63 + $e_user = null; 64 + } 65 + } 66 + } 67 + 68 + if (!$errors) { 69 + $name = $user->getRealName(); 70 + $email = $user->loadPrimaryEmailAddress(); 71 + 72 + $signature = id(new LegalpadDocumentSignature()) 73 + ->setDocumentPHID($document->getPHID()) 74 + ->setDocumentVersion($document->getVersions()) 75 + ->setSignerPHID($user->getPHID()) 76 + ->setSignerName($name) 77 + ->setSignerEmail($email) 78 + ->setIsExemption(1) 79 + ->setExemptionPHID($viewer->getPHID()) 80 + ->setVerified(LegalpadDocumentSignature::VERIFIED) 81 + ->setSignatureData( 82 + array( 83 + 'name' => $name, 84 + 'email' => $email, 85 + 'notes' => $v_notes, 86 + )); 87 + 88 + $signature->save(); 89 + 90 + return id(new AphrontRedirectResponse())->setURI($next_uri); 91 + } 92 + } 93 + 94 + $user_handles = $this->loadViewerHandles($v_users); 95 + 96 + $form = id(new AphrontFormView()) 97 + ->setUser($viewer) 98 + ->appendChild( 99 + id(new AphrontFormTokenizerControl()) 100 + ->setLabel(pht('Exempt User')) 101 + ->setName('users') 102 + ->setLimit(1) 103 + ->setDatasource('/typeahead/common/users/') 104 + ->setValue($user_handles) 105 + ->setError($e_user)) 106 + ->appendChild( 107 + id(new AphrontFormTextAreaControl()) 108 + ->setLabel(pht('Notes')) 109 + ->setName('notes') 110 + ->setValue($v_notes)); 111 + 112 + return $this->newDialog() 113 + ->setTitle(pht('Add Signature Exemption')) 114 + ->setWidth(AphrontDialogView::WIDTH_FORM) 115 + ->setErrors($errors) 116 + ->appendParagraph( 117 + pht( 118 + 'You can record a signature exemption if a user has signed an '. 119 + 'equivalent document. Other applications will behave as through the '. 120 + 'user has signed this document.')) 121 + ->appendParagraph(null) 122 + ->appendChild($form->buildLayoutView()) 123 + ->addSubmitButton(pht('Add Exemption')) 124 + ->addCancelButton($next_uri); 125 + } 126 + 127 + }
+71
src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php
··· 1 + <?php 2 + 3 + final class LegalpadDocumentSignatureViewController extends LegalpadController { 4 + 5 + private $id; 6 + 7 + public function willProcessRequest(array $data) { 8 + $this->id = $data['id']; 9 + } 10 + 11 + public function processRequest() { 12 + $request = $this->getRequest(); 13 + $viewer = $request->getUser(); 14 + 15 + $signature = id(new LegalpadDocumentSignatureQuery()) 16 + ->setViewer($viewer) 17 + ->withIDs(array($this->id)) 18 + ->executeOne(); 19 + if (!$signature) { 20 + return new Aphront404Response(); 21 + } 22 + 23 + 24 + // NOTE: In order to see signature details (which include the relatively 25 + // internal-feeling "notes" field) you must be able to edit the document. 26 + // Essentially, this power is for document managers. Notably, this prevents 27 + // users from seeing notes about their own exemptions by guessing their 28 + // signature ID. This is purely a policy check. 29 + 30 + $document = id(new LegalpadDocumentQuery()) 31 + ->setViewer($viewer) 32 + ->withIDs(array($signature->getDocument()->getID())) 33 + ->requireCapabilities( 34 + array( 35 + PhabricatorPolicyCapability::CAN_VIEW, 36 + PhabricatorPolicyCapability::CAN_EDIT, 37 + )) 38 + ->executeOne(); 39 + if (!$document) { 40 + return new Aphront404Response(); 41 + } 42 + 43 + 44 + $document_id = $signature->getDocument()->getID(); 45 + $next_uri = $this->getApplicationURI('signatures/'.$document_id.'/'); 46 + 47 + $exemption_phid = $signature->getExemptionPHID(); 48 + $handles = $this->loadViewerHandles(array($exemption_phid)); 49 + $exemptor_handle = $handles[$exemption_phid]; 50 + 51 + $data = $signature->getSignatureData(); 52 + 53 + $form = id(new AphrontFormView()) 54 + ->setUser($viewer) 55 + ->appendChild( 56 + id(new AphrontFormMarkupControl()) 57 + ->setLabel(pht('Exemption By')) 58 + ->setValue($exemptor_handle->renderLink())) 59 + ->appendChild( 60 + id(new AphrontFormMarkupControl()) 61 + ->setLabel(pht('Notes')) 62 + ->setValue(idx($data, 'notes'))); 63 + 64 + return $this->newDialog() 65 + ->setTitle(pht('Signature Details')) 66 + ->setWidth(AphrontDialogView::WIDTH_FORM) 67 + ->appendChild($form->buildLayoutView()) 68 + ->addCancelButton($next_uri, pht('Close')); 69 + } 70 + 71 + }
+33 -2
src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php
··· 178 178 'red', 179 179 pht('Unverified Email')); 180 180 181 + $sig_exemption = $this->renderIcon( 182 + 'fa-asterisk', 183 + 'indigo', 184 + pht('Exemption')); 185 + 181 186 id(new PHUIIconView()) 182 187 ->setIconFont('fa-envelope', 'red') 183 188 ->addSigil('has-tooltip') ··· 190 195 191 196 $document = $signature->getDocument(); 192 197 193 - if (!$signature->isVerified()) { 198 + if ($signature->getIsExemption()) { 199 + $signature_href = $this->getApplicationURI( 200 + 'signature/'.$signature->getID().'/'); 201 + 202 + $sig_icon = javelin_tag( 203 + 'a', 204 + array( 205 + 'href' => $signature_href, 206 + 'sigil' => 'workflow', 207 + ), 208 + $sig_exemption); 209 + } else if (!$signature->isVerified()) { 194 210 $sig_icon = $sig_unverified; 195 211 } else if ($signature->getDocumentVersion() != $document->getVersions()) { 196 212 $sig_icon = $sig_old; ··· 242 258 'right', 243 259 )); 244 260 261 + $header = id(new PHUIHeaderView()) 262 + ->setHeader(pht('Signatures')); 263 + 264 + if ($this->document) { 265 + $document_id = $this->document->getID(); 266 + 267 + $header->addActionLink( 268 + id(new PHUIButtonView()) 269 + ->setText(pht('Add Signature Exemption')) 270 + ->setTag('a') 271 + ->setHref($this->getApplicationURI('addsignature/'.$document_id.'/')) 272 + ->setWorkflow(true) 273 + ->setIcon(id(new PHUIIconView())->setIconFont('fa-pencil'))); 274 + } 275 + 245 276 $box = id(new PHUIObjectBoxView()) 246 - ->setHeaderText(pht('Signatures')) 277 + ->setHeader($header) 247 278 ->appendChild($table); 248 279 249 280 if (!$this->document) {
+2
src/applications/legalpad/storage/LegalpadDocumentSignature.php
··· 14 14 protected $signerEmail; 15 15 protected $signatureData = array(); 16 16 protected $verified; 17 + protected $isExemption = 0; 18 + protected $exemptionPHID; 17 19 protected $secretKey; 18 20 19 21 private $document = self::ATTACHABLE;
+21
src/docs/user/userguide/legalpad.diviner
··· 61 61 Users will now only be able to take the action (for example, view or edit the 62 62 object) if they have signed the specified documents. 63 63 64 + 65 + Adding Exemptions 66 + ================= 67 + 68 + If you have users who have signed an alternate form of a document (for example, 69 + you have a hard copy on file), or an equivalent document, or who are otherwise 70 + exempt from needing to sign a document in Legalpad, you can add a signature 71 + exemption for them. 72 + 73 + Other applications will treat users with a signature exemption as though they 74 + had signed the document, although the UI will show the signature as an exemption 75 + rather than a normal signature. 76 + 77 + To add an exemption, go to **Manage Document**, then **View Signatures**, then 78 + **Add Signature Exemption**. 79 + 80 + You can optionally add notes about why a user is exempt from signing a document. 81 + To review the notes later (and see who added the exemption), click the colored 82 + asterisk in the list view. 83 + 84 + 64 85 Roadmap 65 86 ======== 66 87