diff --git a/resources/sql/autopatches/20140703.legalcorp.1.sql b/resources/sql/autopatches/20140703.legalcorp.1.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140703.legalcorp.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_document + ADD signatureType VARCHAR(4) NOT NULL COLLATE utf8_bin; diff --git a/resources/sql/autopatches/20140703.legalcorp.2.sql b/resources/sql/autopatches/20140703.legalcorp.2.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140703.legalcorp.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_legalpad.legalpad_document + SET signatureType = 'user' WHERE signatureType = ''; diff --git a/resources/sql/autopatches/20140703.legalcorp.3.sql b/resources/sql/autopatches/20140703.legalcorp.3.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140703.legalcorp.3.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD signatureType VARCHAR(4) NOT NULL COLLATE utf8_bin AFTER documentVersion; diff --git a/resources/sql/autopatches/20140703.legalcorp.4.sql b/resources/sql/autopatches/20140703.legalcorp.4.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140703.legalcorp.4.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_legalpad.legalpad_documentsignature + SET signatureType = 'user' WHERE signatureType = ''; diff --git a/resources/sql/autopatches/20140703.legalcorp.5.sql b/resources/sql/autopatches/20140703.legalcorp.5.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20140703.legalcorp.5.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + CHANGE signerPHID signerPHID VARCHAR(64) COLLATE utf8_bin; diff --git a/src/applications/legalpad/constants/LegalpadTransactionType.php b/src/applications/legalpad/constants/LegalpadTransactionType.php --- a/src/applications/legalpad/constants/LegalpadTransactionType.php +++ b/src/applications/legalpad/constants/LegalpadTransactionType.php @@ -1,11 +1,9 @@ setCreatorPHID($user->getPHID()); $document->attachDocumentBody($body); $document->setDocumentBodyPHID(PhabricatorPHIDConstants::PHID_VOID); - $title = null; - $text = null; } else { $is_create = false; @@ -44,12 +39,15 @@ if (!$document) { return new Aphront404Response(); } - $title = $document->getDocumentBody()->getTitle(); - $text = $document->getDocumentBody()->getText(); } $e_title = true; $e_text = true; + + $title = $document->getDocumentBody()->getTitle(); + $text = $document->getDocumentBody()->getText(); + $v_signature_type = $document->getSignatureType(); + $errors = array(); $can_view = null; $can_edit = null; @@ -86,6 +84,13 @@ ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($can_edit); + if ($is_create) { + $v_signature_type = $request->getStr('signatureType'); + $xactions[] = id(new LegalpadTransaction()) + ->setTransactionType(LegalpadTransactionType::TYPE_SIGNATURE_TYPE) + ->setNewValue($v_signature_type); + } + if (!$errors) { $editor = id(new LegalpadDocumentEditor()) ->setContentSourceFromRequest($request) @@ -113,7 +118,23 @@ ->setLabel(pht('Title')) ->setError($e_title) ->setValue($title) - ->setName('title')) + ->setName('title')); + + if ($is_create) { + $form->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Who Should Sign?')) + ->setName(pht('signatureType')) + ->setValue($v_signature_type) + ->setOptions(LegalpadDocument::getSignatureTypeMap())); + } else { + $form->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Who Should Sign?')) + ->setValue($document->getSignatureTypeName())); + } + + $form ->appendChild( id(new PhabricatorRemarkupControl()) ->setID('document-text') diff --git a/src/applications/legalpad/controller/LegalpadDocumentManageController.php b/src/applications/legalpad/controller/LegalpadDocumentManageController.php --- a/src/applications/legalpad/controller/LegalpadDocumentManageController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentManageController.php @@ -174,6 +174,10 @@ ->setActionList($actions); $properties->addProperty( + pht('Signature Type'), + $document->getSignatureTypeName()); + + $properties->addProperty( pht('Last Updated'), phabricator_datetime($document->getDateModified(), $user)); diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -25,107 +25,116 @@ return new Aphront404Response(); } - $signer_phid = null; - $signature_data = array(); - if ($viewer->isLoggedIn()) { - $signer_phid = $viewer->getPHID(); - $signature_data = array( - 'name' => $viewer->getRealName(), - 'email' => $viewer->loadPrimaryEmailAddress(), - ); - } else if ($request->isFormPost()) { - $email = new PhutilEmailAddress($request->getStr('email')); - if (strlen($email->getDomainName())) { - $email_obj = id(new PhabricatorUserEmail()) - ->loadOneWhere('address = %s', $email->getAddress()); - if ($email_obj) { - return $this->signInResponse(); - } - $external_account = id(new PhabricatorExternalAccountQuery()) - ->setViewer($viewer) - ->withAccountTypes(array('email')) - ->withAccountDomains(array($email->getDomainName())) - ->withAccountIDs(array($email->getAddress())) - ->loadOneOrCreate(); - if ($external_account->getUserPHID()) { - return $this->signInResponse(); + list($signer_phid, $signature_data) = $this->readSignerInformation( + $document, + $request); + + $signature = null; + + $type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; + $is_individual = ($document->getSignatureType() == $type_individual); + if ($is_individual) { + if ($signer_phid) { + // TODO: This is odd and should probably be adjusted after grey/external + // accounts work better, but use the omnipotent viewer to check for a + // signature so we can pick up anonymous/grey signatures. + + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withDocumentPHIDs(array($document->getPHID())) + ->withSignerPHIDs(array($signer_phid)) + ->executeOne(); + + if ($signature && !$viewer->isLoggedIn()) { + return $this->newDialog() + ->setTitle(pht('Already Signed')) + ->appendParagraph(pht('You have already signed this document!')) + ->addCancelButton('/'.$document->getMonogram(), pht('Okay')); } - $signer_phid = $external_account->getPHID(); } - } - $signature = null; - if ($signer_phid) { - // TODO: This is odd and should probably be adjusted after grey/external - // accounts work better, but use the omnipotent viewer to check for a - // signature so we can pick up anonymous/grey signatures. - - $signature = id(new LegalpadDocumentSignatureQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withDocumentPHIDs(array($document->getPHID())) - ->withSignerPHIDs(array($signer_phid)) - ->executeOne(); - - if ($signature && !$viewer->isLoggedIn()) { - return $this->newDialog() - ->setTitle(pht('Already Signed')) - ->appendParagraph(pht('You have already signed this document!')) - ->addCancelButton('/'.$document->getMonogram(), pht('Okay')); + $signed_status = null; + if (!$signature) { + $has_signed = false; + $signature = id(new LegalpadDocumentSignature()) + ->setSignerPHID($signer_phid) + ->setDocumentPHID($document->getPHID()) + ->setDocumentVersion($document->getVersions()); + + // If the user is logged in, show a notice that they haven't signed. + // If they aren't logged in, we can't be as sure, so don't show + // anything. + if ($viewer->isLoggedIn()) { + $signed_status = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_WARNING) + ->setErrors( + array( + pht('You have not signed this document yet.'), + )); + } + } else { + $has_signed = true; + $signature_data = $signature->getSignatureData(); + + // In this case, we know they've signed. + $signed_at = $signature->getDateCreated(); + + if ($signature->getIsExemption()) { + $exemption_phid = $signature->getExemptionPHID(); + $handles = $this->loadViewerHandles(array($exemption_phid)); + $exemption_handle = $handles[$exemption_phid]; + + $signed_text = pht( + 'You do not need to sign this document. '. + '%s added a signature exemption for you on %s.', + $exemption_handle->renderLink(), + phabricator_datetime($signed_at, $viewer)); + } else { + $signed_text = pht( + 'You signed this document on %s.', + phabricator_datetime($signed_at, $viewer)); + } + + $signed_status = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setErrors(array($signed_text)); } - } - $signed_status = null; - if (!$signature) { - $has_signed = false; + $field_errors = array( + 'name' => true, + 'email' => true, + 'agree' => true, + ); + } else { $signature = id(new LegalpadDocumentSignature()) - ->setSignerPHID($signer_phid) ->setDocumentPHID($document->getPHID()) - ->setDocumentVersion($document->getVersions()) - ->setSignerName((string)idx($signature_data, 'name')) - ->setSignerEmail((string)idx($signature_data, 'email')) - ->setSignatureData($signature_data); + ->setDocumentVersion($document->getVersions()); - // If the user is logged in, show a notice that they haven't signed. - // If they aren't logged in, we can't be as sure, so don't show anything. if ($viewer->isLoggedIn()) { + $has_signed = false; + + $signed_status = null; + } else { + // This just hides the form. + $has_signed = true; + + $login_text = pht( + 'This document requires a corporate signatory. You must log in to '. + 'accept this document on behalf of a company you represent.'); $signed_status = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) - ->setErrors( - array( - pht('You have not signed this document yet.'), - )); - } - } else { - $has_signed = true; - $signature_data = $signature->getSignatureData(); - - // In this case, we know they've signed. - $signed_at = $signature->getDateCreated(); - - if ($signature->getIsExemption()) { - $exemption_phid = $signature->getExemptionPHID(); - $handles = $this->loadViewerHandles(array($exemption_phid)); - $exemption_handle = $handles[$exemption_phid]; - - $signed_text = pht( - 'You do not need to sign this document. '. - '%s added a signature exemption for you on %s.', - $exemption_handle->renderLink(), - phabricator_datetime($signed_at, $viewer)); - } else { - $signed_text = pht( - 'You signed this document on %s.', - phabricator_datetime($signed_at, $viewer)); + ->setErrors(array($login_text)); } - $signed_status = id(new AphrontErrorView()) - ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) - ->setErrors(array($signed_text)); + $field_errors = array( + 'name' => true, + 'address' => true, + 'contact.name' => true, + 'email' => true, + ); } - $e_name = true; - $e_email = true; - $e_agree = null; + $signature->setSignatureData($signature_data); $errors = array(); if ($request->isFormOrHisecPost() && !$has_signed) { @@ -139,50 +148,25 @@ '/'.$document->getMonogram()); } - $name = $request->getStr('name'); - $agree = $request->getExists('agree'); - - if (!strlen($name)) { - $e_name = pht('Required'); - $errors[] = pht('Name field is required.'); - } else { - $e_name = null; - } - $signature_data['name'] = $name; - - if ($viewer->isLoggedIn()) { - $email = $viewer->loadPrimaryEmailAddress(); - } else { - $email = $request->getStr('email'); + list($form_data, $errors, $field_errors) = $this->readSignatureForm( + $document, + $request); - $addr_obj = null; - if (!strlen($email)) { - $e_email = pht('Required'); - $errors[] = pht('Email field is required.'); - } else { - $addr_obj = new PhutilEmailAddress($email); - $domain = $addr_obj->getDomainName(); - if (!$domain) { - $e_email = pht('Invalid'); - $errors[] = pht('A valid email is required.'); - } else { - $e_email = null; - } - } - } - $signature_data['email'] = $email; + $signature_data = $form_data + $signature_data; + $signature->setSignatureData($signature_data); + $signature->setSignatureType($document->getSignatureType()); $signature->setSignerName((string)idx($signature_data, 'name')); $signature->setSignerEmail((string)idx($signature_data, 'email')); - $signature->setSignatureData($signature_data); + $agree = $request->getExists('agree'); if (!$agree) { $errors[] = pht( 'You must check "I agree to the terms laid forth above."'); - $e_agree = pht('Required'); + $field_errors['agree'] = pht('Required'); } - if ($viewer->isLoggedIn()) { + if ($viewer->isLoggedIn() && $is_individual) { $verified = LegalpadDocumentSignature::VERIFIED; } else { $verified = LegalpadDocumentSignature::UNVERIFIED; @@ -195,9 +179,13 @@ // If the viewer is logged in, send them to the document page, which // will show that they have signed the document. Otherwise, send them // to a completion page. - if ($viewer->isLoggedIn()) { + if ($viewer->isLoggedIn() && $is_individual) { $next_uri = '/'.$document->getMonogram(); } else { + $this->sendVerifySignatureEmail( + $document, + $signature); + $next_uri = $this->getApplicationURI('done/'); } @@ -257,11 +245,9 @@ } $signature_form = $this->buildSignatureForm( - $document_body, + $document, $signature, - $e_name, - $e_email, - $e_agree); + $field_errors); $subheader = id(new PHUIHeaderView()) ->setHeader(pht('Agree and Sign Document')) @@ -289,72 +275,343 @@ )); } + private function readSignerInformation( + LegalpadDocument $document, + AphrontRequest $request) { + + $viewer = $request->getUser(); + $signer_phid = null; + $signature_data = array(); + + switch ($document->getSignatureType()) { + case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: + if ($viewer->isLoggedIn()) { + $signer_phid = $viewer->getPHID(); + $signature_data = array( + 'name' => $viewer->getRealName(), + 'email' => $viewer->loadPrimaryEmailAddress(), + ); + } else if ($request->isFormPost()) { + $email = new PhutilEmailAddress($request->getStr('email')); + if (strlen($email->getDomainName())) { + $email_obj = id(new PhabricatorUserEmail()) + ->loadOneWhere('address = %s', $email->getAddress()); + if ($email_obj) { + return $this->signInResponse(); + } + $external_account = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withAccountTypes(array('email')) + ->withAccountDomains(array($email->getDomainName())) + ->withAccountIDs(array($email->getAddress())) + ->loadOneOrCreate(); + if ($external_account->getUserPHID()) { + return $this->signInResponse(); + } + $signer_phid = $external_account->getPHID(); + } + } + break; + case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: + $signer_phid = $viewer->getPHID(); + if ($signer_phid) { + $signature_data = array( + 'contact.name' => $viewer->getRealName(), + 'email' => $viewer->loadPrimaryEmailAddress(), + 'actorPHID' => $viewer->getPHID(), + ); + } + break; + } + + return array($signer_phid, $signature_data); + } + private function buildSignatureForm( - LegalpadDocumentBody $body, + LegalpadDocument $document, LegalpadDocumentSignature $signature, - $e_name, - $e_email, - $e_agree) { + array $errors) { $viewer = $this->getRequest()->getUser(); $data = $signature->getSignatureData(); $form = id(new AphrontFormView()) - ->setUser($viewer) + ->setUser($viewer); + + $signature_type = $document->getSignatureType(); + switch ($signature_type) { + case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: + $this->buildIndividualSignatureForm( + $form, + $document, + $signature, + $errors); + break; + case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: + $this->buildCorporateSignatureForm( + $form, + $document, + $signature, + $errors); + break; + default: + throw new Exception( + pht( + 'This document has an unknown signature type ("%s").', + $signature_type)); + } + + $form + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->setError(idx($errors, 'agree', null)) + ->addCheckbox( + 'agree', + 'agree', + pht('I agree to the terms laid forth above.'), + false)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Sign Document')) + ->addCancelButton($this->getApplicationURI())); + + return $form; + } + + private function buildIndividualSignatureForm( + AphrontFormView $form, + LegalpadDocument $document, + LegalpadDocumentSignature $signature, + array $errors) { + + $data = $signature->getSignatureData(); + + $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setValue(idx($data, 'name', '')) ->setName('name') - ->setError($e_name)); + ->setError(idx($errors, 'name', null))); + $viewer = $this->getRequest()->getUser(); if (!$viewer->isLoggedIn()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setValue(idx($data, 'email', '')) ->setName('email') - ->setError($e_email)); + ->setError(idx($errors, 'email', null))); } + return $form; + } + + private function buildCorporateSignatureForm( + AphrontFormView $form, + LegalpadDocument $document, + LegalpadDocumentSignature $signature, + array $errors) { + + $data = $signature->getSignatureData(); + $form ->appendChild( - id(new AphrontFormCheckboxControl()) - ->setError($e_agree) - ->addCheckbox( - 'agree', - 'agree', - pht('I agree to the terms laid forth above.'), - false)) + id(new AphrontFormTextControl()) + ->setLabel(pht('Company Name')) + ->setValue(idx($data, 'name', '')) + ->setName('name') + ->setError(idx($errors, 'name', null))) ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Sign Document')) - ->addCancelButton($this->getApplicationURI())); + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Company Address')) + ->setValue(idx($data, 'address', '')) + ->setName('address') + ->setError(idx($errors, 'address', null))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Contact Name')) + ->setValue(idx($data, 'contact.name', '')) + ->setName('contact.name') + ->setError(idx($errors, 'contact.name', null))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Contact Email')) + ->setValue(idx($data, 'email', '')) + ->setName('email') + ->setError(idx($errors, 'email', null))); return $form; } + private function readSignatureForm( + LegalpadDocument $document, + AphrontRequest $request) { + + $signature_type = $document->getSignatureType(); + switch ($signature_type) { + case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL: + $result = $this->readIndividualSignatureForm( + $document, + $request); + break; + case LegalpadDocument::SIGNATURE_TYPE_CORPORATION: + $result = $this->readCorporateSignatureForm( + $document, + $request); + break; + default: + throw new Exception( + pht( + 'This document has an unknown signature type ("%s").', + $signature_type)); + } + + return $result; + } + + private function readIndividualSignatureForm( + LegalpadDocument $document, + AphrontRequest $request) { + + $signature_data = array(); + $errors = array(); + $field_errors = array(); + + + $name = $request->getStr('name'); + + if (!strlen($name)) { + $field_errors['name'] = pht('Required'); + $errors[] = pht('Name field is required.'); + } else { + $field_errors['name'] = null; + } + $signature_data['name'] = $name; + + $viewer = $request->getUser(); + if ($viewer->isLoggedIn()) { + $email = $viewer->loadPrimaryEmailAddress(); + } else { + $email = $request->getStr('email'); + + $addr_obj = null; + if (!strlen($email)) { + $field_errors['email'] = pht('Required'); + $errors[] = pht('Email field is required.'); + } else { + $addr_obj = new PhutilEmailAddress($email); + $domain = $addr_obj->getDomainName(); + if (!$domain) { + $field_errors['email'] = pht('Invalid'); + $errors[] = pht('A valid email is required.'); + } else { + $field_errors['email'] = null; + } + } + } + $signature_data['email'] = $email; + + return array($signature_data, $errors, $field_errors); + } + + private function readCorporateSignatureForm( + LegalpadDocument $document, + AphrontRequest $request) { + + $viewer = $request->getUser(); + if (!$viewer->isLoggedIn()) { + throw new Exception( + pht( + 'You can not sign a document on behalf of a corporation unless '. + 'you are logged in.')); + } + + $signature_data = array(); + $errors = array(); + $field_errors = array(); + + $name = $request->getStr('name'); + + if (!strlen($name)) { + $field_errors['name'] = pht('Required'); + $errors[] = pht('Company name is required.'); + } else { + $field_errors['name'] = null; + } + $signature_data['name'] = $name; + + $address = $request->getStr('address'); + if (!strlen($address)) { + $field_errors['address'] = pht('Required'); + $errors[] = pht('Company address is required.'); + } else { + $field_errors['address'] = null; + } + $signature_data['address'] = $address; + + $contact_name = $request->getStr('contact.name'); + if (!strlen($contact_name)) { + $field_errors['contact.name'] = pht('Required'); + $errors[] = pht('Contact name is required.'); + } else { + $field_errors['contact.name'] = null; + } + $signature_data['contact.name'] = $contact_name; + + $email = $request->getStr('email'); + $addr_obj = null; + if (!strlen($email)) { + $field_errors['email'] = pht('Required'); + $errors[] = pht('Contact email is required.'); + } else { + $addr_obj = new PhutilEmailAddress($email); + $domain = $addr_obj->getDomainName(); + if (!$domain) { + $field_errors['email'] = pht('Invalid'); + $errors[] = pht('A valid email is required.'); + } else { + $field_errors['email'] = null; + } + } + $signature_data['email'] = $email; + + return array($signature_data, $errors, $field_errors); + } + private function sendVerifySignatureEmail( LegalpadDocument $doc, LegalpadDocumentSignature $signature) { $signature_data = $signature->getSignatureData(); $email = new PhutilEmailAddress($signature_data['email']); - $doc_link = PhabricatorEnv::getProductionURI($doc->getMonogram()); + $doc_name = $doc->getTitle(); + $doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram()); $path = $this->getApplicationURI(sprintf( '/verify/%s/', $signature->getSecretKey())); $link = PhabricatorEnv::getProductionURI($path); + $name = idx($signature_data, 'name'); + $body = <<getApplicationURI('signatures/'.$document->getID().'/'); + $e_name = true; $e_user = true; $v_users = array(); $v_notes = ''; + $v_name = ''; $errors = array(); + $type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL; + $is_individual = ($document->getSignatureType() == $type_individual); + if ($request->isFormPost()) { $v_notes = $request->getStr('notes'); $v_users = array_slice($request->getArr('users'), 0, 1); + $v_name = $request->getStr('name'); - $user_phid = head($v_users); - if (!$user_phid) { - $e_user = pht('Required'); - $errors[] = pht('You must choose a user to exempt.'); - } else { - $user = id(new PhabricatorPeopleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($user_phid)) - ->executeOne(); - - if (!$user) { - $e_user = pht('Invalid'); - $errors[] = pht('That user does not exist.'); + if ($is_individual) { + $user_phid = head($v_users); + if (!$user_phid) { + $e_user = pht('Required'); + $errors[] = pht('You must choose a user to exempt.'); } else { - $signature = id(new LegalpadDocumentSignatureQuery()) + $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) - ->withDocumentPHIDs(array($document->getPHID())) - ->withSignerPHIDs(array($user->getPHID())) + ->withPHIDs(array($user_phid)) ->executeOne(); - if ($signature) { - $e_user = pht('Signed'); - $errors[] = pht('That user has already signed this document.'); + + if (!$user) { + $e_user = pht('Invalid'); + $errors[] = pht('That user does not exist.'); } else { - $e_user = null; + $signature = id(new LegalpadDocumentSignatureQuery()) + ->setViewer($viewer) + ->withDocumentPHIDs(array($document->getPHID())) + ->withSignerPHIDs(array($user->getPHID())) + ->executeOne(); + if ($signature) { + $e_user = pht('Signed'); + $errors[] = pht('That user has already signed this document.'); + } else { + $e_user = null; + } } } + } else { + $company_name = $v_name; + if (!strlen($company_name)) { + $e_name = pht('Required'); + $errors[] = pht('You must choose a company to add an exemption for.'); + } } if (!$errors) { - $name = $user->getRealName(); - $email = $user->loadPrimaryEmailAddress(); + if ($is_individual) { + $name = $user->getRealName(); + $email = $user->loadPrimaryEmailAddress(); + $signer_phid = $user->getPHID(); + $signature_data = array( + 'name' => $name, + 'email' => $email, + 'notes' => $v_notes, + ); + } else { + $name = $company_name; + $email = ''; + $signer_phid = null; + $signature_data = array( + 'name' => $name, + 'email' => null, + 'notes' => $v_notes, + 'actorPHID' => $viewer->getPHID(), + ); + } $signature = id(new LegalpadDocumentSignature()) ->setDocumentPHID($document->getPHID()) ->setDocumentVersion($document->getVersions()) - ->setSignerPHID($user->getPHID()) + ->setSignerPHID($signer_phid) ->setSignerName($name) ->setSignerEmail($email) + ->setSignatureType($document->getSignatureType()) ->setIsExemption(1) ->setExemptionPHID($viewer->getPHID()) ->setVerified(LegalpadDocumentSignature::VERIFIED) - ->setSignatureData( - array( - 'name' => $name, - 'email' => $email, - 'notes' => $v_notes, - )); + ->setSignatureData($signature_data); $signature->save(); @@ -91,18 +119,31 @@ } } - $user_handles = $this->loadViewerHandles($v_users); - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setLabel(pht('Exempt User')) - ->setName('users') - ->setLimit(1) - ->setDatasource('/typeahead/common/users/') - ->setValue($user_handles) - ->setError($e_user)) + ->setUser($viewer); + + if ($is_individual) { + $user_handles = $this->loadViewerHandles($v_users); + $form + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Exempt User')) + ->setName('users') + ->setLimit(1) + ->setDatasource('/typeahead/common/users/') + ->setValue($user_handles) + ->setError($e_user)); + } else { + $form + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Company Name')) + ->setName('name') + ->setError($e_name) + ->setValue($v_name)); + } + + $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Notes')) diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php --- a/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureVerificationController.php @@ -1,93 +1,99 @@ code = $data['code']; - } - - public function shouldRequireEmailVerification() { - return false; + public function shouldAllowPublic() { + return true; } - public function shouldRequireLogin() { - return false; + public function willProcessRequest(array $data) { + $this->code = $data['code']; } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); - - // this page can be accessed by not logged in users to valid their - // signatures. use the omnipotent user for these cases. - if (!$user->isLoggedIn()) { - $viewer = PhabricatorUser::getOmnipotentUser(); - } else { - $viewer = $user; - } + $viewer = $request->getUser(); + // NOTE: We're using the omnipotent user to handle logged-out signatures + // and corporate signatures. $signature = id(new LegalpadDocumentSignatureQuery()) - ->setViewer($viewer) + ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withSecretKeys(array($this->code)) ->executeOne(); if (!$signature) { - $title = pht('Unable to Verify Signature'); - $content = pht( - 'The verification code you provided is incorrect or the signature '. - 'has been removed. '. - 'Make sure you followed the link in the email correctly.'); - $uri = $this->getApplicationURI(); - $continue = pht('Rats!'); - } else { - $document = id(new LegalpadDocumentQuery()) - ->setViewer($user) - ->withPHIDs(array($signature->getDocumentPHID())) - ->executeOne(); - // the document could be deleted or have its permissions changed - // 4oh4 time - if (!$document) { - return new Aphront404Response(); - } - $uri = '/'.$document->getMonogram(); - if ($signature->isVerified()) { - $title = pht('Signature Already Verified'); - $content = pht( - 'This signature has already been verified.'); - $continue = pht('Continue to Legalpad Document'); - } else { - $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); - $signature - ->setVerified(LegalpadDocumentSignature::VERIFIED) - ->save(); - unset($guard); - $title = pht('Signature Verified'); - $content = pht('The signature is now verified.'); - $continue = pht('Continue to Legalpad Document'); - } + return $this->newDialog() + ->setTitle(pht('Unable to Verify Signature')) + ->appendParagraph( + pht( + 'The signature verification code is incorrect, or the signature '. + 'has been invalidated. Make sure you followed the link in the '. + 'email correctly.')) + ->addCancelButton('/', pht('Rats!')); } - $dialog = id(new AphrontDialogView()) - ->setUser($user) - ->setTitle($title) - ->setMethod('GET') - ->addCancelButton($uri, $continue) - ->appendChild($content); + if ($signature->isVerified()) { + return $this->newDialog() + ->setTitle(pht('Signature Already Verified')) + ->appendParagraph( + pht( + 'This signature has already been verified.')) + ->addCancelButton('/', pht('Okay')); + } - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Verify Signature')); + if ($request->isFormPost()) { + $signature + ->setVerified(LegalpadDocumentSignature::VERIFIED) + ->save(); - return $this->buildApplicationPage( + return $this->newDialog() + ->setTitle(pht('Signature Verified')) + ->appendParagraph(pht('The signature is now verified.')) + ->addCancelButton('/', pht('Okay')); + } + + $document_link = phutil_tag( + 'a', array( - $crumbs, - $dialog, + 'href' => '/'.$signature->getDocument()->getMonogram(), + 'target' => '_blank', ), - array( - 'title' => pht('Verify Signature'), - )); + $signature->getDocument()->getTitle()); + + $signed_at = phabricator_datetime($signature->getDateCreated(), $viewer); + + $name = $signature->getSignerName(); + $email = $signature->getSignerEmail(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendRemarkupInstructions( + pht('Please verify this document signature.')) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Document')) + ->setValue($document_link)) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Signed At')) + ->setValue($signed_at)) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Name')) + ->setValue($name)) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Email')) + ->setValue($email)); + + return $this->newDialog() + ->setTitle(pht('Verify Signature?')) + ->appendChild($form->buildLayoutView()) + ->addCancelButton('/') + ->addSubmitButton(pht('Verify Signature')); } } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php --- a/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignatureViewController.php @@ -44,22 +44,63 @@ $document_id = $signature->getDocument()->getID(); $next_uri = $this->getApplicationURI('signatures/'.$document_id.'/'); + $data = $signature->getSignatureData(); + $exemption_phid = $signature->getExemptionPHID(); - $handles = $this->loadViewerHandles(array($exemption_phid)); + $actor_phid = idx($data, 'actorPHID'); + $handles = $this->loadViewerHandles( + array( + $exemption_phid, + $actor_phid, + )); $exemptor_handle = $handles[$exemption_phid]; - - $data = $signature->getSignatureData(); + $actor_handle = $handles[$actor_phid]; $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel(pht('Exemption By')) - ->setValue($exemptor_handle->renderLink())) - ->appendChild( - id(new AphrontFormMarkupControl()) - ->setLabel(pht('Notes')) - ->setValue(idx($data, 'notes'))); + ->setUser($viewer); + + if ($signature->getExemptionPHID()) { + $form + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Exemption By')) + ->setValue($exemptor_handle->renderLink())) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Notes')) + ->setValue(idx($data, 'notes'))); + } + + $type_corporation = LegalpadDocument::SIGNATURE_TYPE_CORPORATION; + if ($signature->getSignatureType() == $type_corporation) { + $form + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Signing User')) + ->setValue($actor_handle->renderLink())) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Company Name')) + ->setValue(idx($data, 'name'))) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Address')) + ->setValue(phutil_escape_html_newlines(idx($data, 'address')))) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Contact Name')) + ->setValue(idx($data, 'contact.name'))) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Contact Email')) + ->setValue( + phutil_tag( + 'a', + array( + 'href' => 'mailto:'.idx($data, 'email'), + ), + idx($data, 'email')))); + } return $this->newDialog() ->setTitle(pht('Signature Details')) diff --git a/src/applications/legalpad/editor/LegalpadDocumentEditor.php b/src/applications/legalpad/editor/LegalpadDocumentEditor.php --- a/src/applications/legalpad/editor/LegalpadDocumentEditor.php +++ b/src/applications/legalpad/editor/LegalpadDocumentEditor.php @@ -11,6 +11,7 @@ private function setIsContribution($is_contribution) { $this->isContribution = $is_contribution; } + private function isContribution() { return $this->isContribution; } @@ -24,6 +25,8 @@ $types[] = LegalpadTransactionType::TYPE_TITLE; $types[] = LegalpadTransactionType::TYPE_TEXT; + $types[] = LegalpadTransactionType::TYPE_SIGNATURE_TYPE; + return $types; } @@ -36,6 +39,8 @@ return $object->getDocumentBody()->getTitle(); case LegalpadTransactionType::TYPE_TEXT: return $object->getDocumentBody()->getText(); + case LegalpadTransactionType::TYPE_SIGNATURE_TYPE: + return $object->getSignatureType(); } } @@ -46,6 +51,7 @@ switch ($xaction->getTransactionType()) { case LegalpadTransactionType::TYPE_TITLE: case LegalpadTransactionType::TYPE_TEXT: + case LegalpadTransactionType::TYPE_SIGNATURE_TYPE: return $xaction->getNewValue(); } } @@ -66,6 +72,9 @@ $body->setText($xaction->getNewValue()); $this->setIsContribution(true); break; + case LegalpadTransactionType::TYPE_SIGNATURE_TYPE: + $object->setSignatureType($xaction->getNewValue()); + break; } } @@ -116,6 +125,7 @@ switch ($type) { case LegalpadTransactionType::TYPE_TITLE: case LegalpadTransactionType::TYPE_TEXT: + case LegalpadTransactionType::TYPE_SIGNATURE_TYPE: return $v; } diff --git a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php --- a/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSearchEngine.php @@ -169,13 +169,18 @@ $title = $document->getTitle(); + $type_name = $document->getSignatureTypeName(); + $type_icon = $document->getSignatureTypeIcon(); + $item = id(new PHUIObjectItemView()) ->setObjectName($document->getMonogram()) ->setHeader($title) ->setHref('/'.$document->getMonogram()) ->setObject($document) - ->addIcon('none', pht('Version %d', $document->getVersions())) - ->addIcon('none', pht('Updated %s', $last_updated)); + ->addIcon($type_icon, $type_name) + ->addIcon( + 'fa-pencil grey', + pht('Version %d (%s)', $document->getVersions(), $last_updated)); if ($viewer->getPHID()) { $signature = $document->getUserSignature($viewer->getPHID()); diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php --- a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php @@ -168,6 +168,11 @@ null, pht('Verified, Current')); + $sig_corp = $this->renderIcon( + 'fa-building-o', + null, + pht('Verified, Corporate')); + $sig_old = $this->renderIcon( 'fa-clock-o', 'orange', @@ -188,6 +193,8 @@ ->addSigil('has-tooltip') ->setMetadata(array('tip' => pht('Unverified Email'))); + $type_corporate = LegalpadDocument::SIGNATURE_TYPE_CORPORATION; + $rows = array(); foreach ($signatures as $signature) { $name = $signature->getSignerName(); @@ -196,28 +203,36 @@ $document = $signature->getDocument(); if ($signature->getIsExemption()) { - $signature_href = $this->getApplicationURI( - 'signature/'.$signature->getID().'/'); - - $sig_icon = javelin_tag( - 'a', - array( - 'href' => $signature_href, - 'sigil' => 'workflow', - ), - $sig_exemption); + $sig_icon = $sig_exemption; } else if (!$signature->isVerified()) { $sig_icon = $sig_unverified; } else if ($signature->getDocumentVersion() != $document->getVersions()) { $sig_icon = $sig_old; + } else if ($signature->getSignatureType() == $type_corporate) { + $sig_icon = $sig_corp; } else { $sig_icon = $sig_good; } + $signature_href = $this->getApplicationURI( + 'signature/'.$signature->getID().'/'); + + $sig_icon = javelin_tag( + 'a', + array( + 'href' => $signature_href, + 'sigil' => 'workflow', + ), + $sig_icon); + + $signer_phid = $signature->getSignerPHID(); + $rows[] = array( $sig_icon, $handles[$document->getPHID()]->renderLink(), - $handles[$signature->getSignerPHID()]->renderLink(), + $signer_phid + ? $handles[$signer_phid]->renderLink() + : null, $name, phutil_tag( 'a', diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -16,6 +16,10 @@ protected $viewPolicy; protected $editPolicy; protected $mailKey; + protected $signatureType; + + const SIGNATURE_TYPE_INDIVIDUAL = 'user'; + const SIGNATURE_TYPE_CORPORATION = 'corp'; private $documentBody = self::ATTACHABLE; private $contributors = self::ATTACHABLE; @@ -37,6 +41,7 @@ ->setContributorCount(0) ->setRecentContributorPHIDs(array()) ->attachSignatures(array()) + ->setSignatureType(self::SIGNATURE_TYPE_INDIVIDUAL) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } @@ -104,6 +109,28 @@ return $this; } + public static function getSignatureTypeMap() { + return array( + self::SIGNATURE_TYPE_INDIVIDUAL => pht('Individuals'), + self::SIGNATURE_TYPE_CORPORATION => pht('Corporations'), + ); + } + + public function getSignatureTypeName() { + $type = $this->getSignatureType(); + return idx(self::getSignatureTypeMap(), $type, $type); + } + + public function getSignatureTypeIcon() { + $type = $this->getSignatureType(); + $map = array( + self::SIGNATURE_TYPE_INDIVIDUAL => 'fa-user grey', + self::SIGNATURE_TYPE_CORPORATION => 'fa-building-o grey', + ); + + return idx($map, $type, 'fa-user grey'); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/legalpad/storage/LegalpadDocumentSignature.php b/src/applications/legalpad/storage/LegalpadDocumentSignature.php --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -9,6 +9,7 @@ protected $documentPHID; protected $documentVersion; + protected $signatureType; protected $signerPHID; protected $signerName; protected $signerEmail;