3 final class LegalpadDocumentSignController
extends LegalpadController
{
5 private $isSessionGate;
7 public function shouldAllowPublic() {
11 public function shouldAllowLegallyNonCompliantUsers() {
15 public function setIsSessionGate($is_session_gate) {
16 $this->isSessionGate
= $is_session_gate;
20 public function getIsSessionGate() {
21 return $this->isSessionGate
;
24 public function handleRequest(AphrontRequest
$request) {
25 $viewer = $request->getUser();
27 $document = id(new LegalpadDocumentQuery())
29 ->withIDs(array($request->getURIData('id')))
30 ->needDocumentBodies(true)
33 return new Aphront404Response();
36 $information = $this->readSignerInformation(
39 if ($information instanceof AphrontResponse
) {
42 list($signer_phid, $signature_data) = $information;
46 $type_individual = LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
;
47 $is_individual = ($document->getSignatureType() == $type_individual);
48 switch ($document->getSignatureType()) {
49 case LegalpadDocument
::SIGNATURE_TYPE_NONE
:
50 // nothing to sign means this should be true
52 // this is a status UI element
53 $signed_status = null;
55 case LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
:
57 // TODO: This is odd and should probably be adjusted after
58 // grey/external accounts work better, but use the omnipotent
59 // viewer to check for a signature so we can pick up
60 // anonymous/grey signatures.
62 $signature = id(new LegalpadDocumentSignatureQuery())
63 ->setViewer(PhabricatorUser
::getOmnipotentUser())
64 ->withDocumentPHIDs(array($document->getPHID()))
65 ->withSignerPHIDs(array($signer_phid))
68 if ($signature && !$viewer->isLoggedIn()) {
69 return $this->newDialog()
70 ->setTitle(pht('Already Signed'))
71 ->appendParagraph(pht('You have already signed this document!'))
72 ->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
76 $signed_status = null;
79 $signature = id(new LegalpadDocumentSignature())
80 ->setSignerPHID($signer_phid)
81 ->setDocumentPHID($document->getPHID())
82 ->setDocumentVersion($document->getVersions());
84 // If the user is logged in, show a notice that they haven't signed.
85 // If they aren't logged in, we can't be as sure, so don't show
87 if ($viewer->isLoggedIn()) {
88 $signed_status = id(new PHUIInfoView())
89 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
92 pht('You have not signed this document yet.'),
97 $signature_data = $signature->getSignatureData();
99 // In this case, we know they've signed.
100 $signed_at = $signature->getDateCreated();
102 if ($signature->getIsExemption()) {
103 $exemption_phid = $signature->getExemptionPHID();
104 $handles = $this->loadViewerHandles(array($exemption_phid));
105 $exemption_handle = $handles[$exemption_phid];
108 'You do not need to sign this document. '.
109 '%s added a signature exemption for you on %s.',
110 $exemption_handle->renderLink(),
111 phabricator_datetime($signed_at, $viewer));
114 'You signed this document on %s.',
115 phabricator_datetime($signed_at, $viewer));
118 $signed_status = id(new PHUIInfoView())
119 ->setSeverity(PHUIInfoView
::SEVERITY_NOTICE
)
120 ->setErrors(array($signed_text));
123 $field_errors = array(
128 $signature->setSignatureData($signature_data);
131 case LegalpadDocument
::SIGNATURE_TYPE_CORPORATION
:
132 $signature = id(new LegalpadDocumentSignature())
133 ->setDocumentPHID($document->getPHID())
134 ->setDocumentVersion($document->getVersions());
136 if ($viewer->isLoggedIn()) {
139 $signed_status = null;
141 // This just hides the form.
145 'This document requires a corporate signatory. You must log in to '.
146 'accept this document on behalf of a company you represent.');
147 $signed_status = id(new PHUIInfoView())
148 ->setSeverity(PHUIInfoView
::SEVERITY_WARNING
)
149 ->setErrors(array($login_text));
152 $field_errors = array(
155 'contact.name' => true,
158 $signature->setSignatureData($signature_data);
164 if ($request->isFormOrHisecPost() && !$has_signed) {
165 list($form_data, $errors, $field_errors) = $this->readSignatureForm(
169 $signature_data = $form_data +
$signature_data;
171 $signature->setSignatureData($signature_data);
172 $signature->setSignatureType($document->getSignatureType());
173 $signature->setSignerName((string)idx($signature_data, 'name'));
174 $signature->setSignerEmail((string)idx($signature_data, 'email'));
176 $agree = $request->getExists('agree');
179 'You must check "I agree to the terms laid forth above."');
180 $field_errors['agree'] = pht('Required');
183 if ($viewer->isLoggedIn() && $is_individual) {
184 $verified = LegalpadDocumentSignature
::VERIFIED
;
186 $verified = LegalpadDocumentSignature
::UNVERIFIED
;
188 $signature->setVerified($verified);
191 // Require MFA to sign legal documents.
192 if ($viewer->isLoggedIn()) {
193 $workflow_key = sprintf(
195 $document->getPHID());
197 $hisec_token = id(new PhabricatorAuthSessionEngine())
198 ->setWorkflowKey($workflow_key)
199 ->requireHighSecurityToken(
202 $document->getURI());
207 // If the viewer is logged in, signing for themselves, send them to
208 // the document page, which will show that they have signed the
209 // document. Unless of course they were required to sign the
210 // document to use Phabricator; in that case try really hard to
211 // re-direct them to where they wanted to go.
213 // Otherwise, send them to a completion page.
214 if ($viewer->isLoggedIn() && $is_individual) {
215 $next_uri = '/'.$document->getMonogram();
216 if ($document->getRequireSignature()) {
217 $request_uri = $request->getRequestURI();
218 $next_uri = (string)$request_uri;
221 $this->sendVerifySignatureEmail(
225 $next_uri = $this->getApplicationURI('done/');
228 return id(new AphrontRedirectResponse())->setURI($next_uri);
232 $document_body = $document->getDocumentBody();
233 $engine = id(new PhabricatorMarkupEngine())
234 ->setViewer($viewer);
237 LegalpadDocumentBody
::MARKUP_FIELD_TEXT
);
240 $document_markup = $engine->getOutput(
242 LegalpadDocumentBody
::MARKUP_FIELD_TEXT
);
244 $title = $document_body->getTitle();
246 $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
248 $can_edit = PhabricatorPolicyFilter
::hasCapability(
251 PhabricatorPolicyCapability
::CAN_EDIT
);
253 // Use the last content update as the modified date. We don't want to
254 // show that a document like a TOS was "updated" by an incidental change
255 // to a field like the preamble or privacy settings which does not actually
256 // affect the content of the agreement.
257 $content_updated = $document_body->getDateCreated();
259 // NOTE: We're avoiding `setPolicyObject()` here so we don't pick up
260 // extra UI elements that are unnecessary and clutter the signature page.
261 // These details are available on the "Manage" page.
262 $header = id(new PHUIHeaderView())
265 ->setEpoch($content_updated);
267 // If we're showing the user this document because it's required to use
268 // Phabricator and they haven't signed it, don't show the "Manage" button,
269 // since it won't work.
270 $is_gate = $this->getIsSessionGate();
272 $header->addActionLink(
273 id(new PHUIButtonView())
275 ->setIcon('fa-pencil')
276 ->setText(pht('Manage'))
277 ->setHref($manage_uri)
278 ->setDisabled(!$can_edit)
279 ->setWorkflow(!$can_edit));
282 $preamble_box = null;
283 if (strlen($document->getPreamble())) {
284 $preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
286 // NOTE: We're avoiding `setObject()` here so we don't pick up extra UI
287 // elements like "Subscribers". This information is available on the
288 // "Manage" page, but just clutters up the "Signature" page.
289 $preamble = id(new PHUIPropertyListView())
291 ->addSectionHeader(pht('Preamble'))
292 ->addTextContent($preamble_text);
294 $preamble_box = new PHUIPropertyGroupView();
295 $preamble_box->addPropertyList($preamble);
298 $content = id(new PHUIDocumentView())
299 ->addClass('legalpad')
308 $signature_box = null;
312 $error_view = id(new PHUIInfoView())
313 ->setErrors($errors);
316 $signature_form = $this->buildSignatureForm(
321 switch ($document->getSignatureType()) {
324 case LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
:
325 case LegalpadDocument
::SIGNATURE_TYPE_CORPORATION
:
326 $box = id(new PHUIObjectBoxView())
327 ->addClass('document-sign-box')
328 ->setHeaderText(pht('Agree and Sign Document'))
329 ->setBackground(PHUIObjectBoxView
::BLUE_PROPERTY
)
330 ->setForm($signature_form);
332 $box->setInfoView($error_view);
334 $signature_box = phutil_tag_div(
335 'phui-document-view-pro-box plt', $box);
342 $crumbs = $this->buildApplicationCrumbs();
343 $crumbs->setBorder(true);
344 $crumbs->addTextCrumb($document->getMonogram());
346 $box = id(new PHUITwoColumnView())
347 ->setFooter($signature_box);
349 return $this->newPage()
352 ->setPageObjectPHIDs(array($document->getPHID()))
359 private function readSignerInformation(
360 LegalpadDocument
$document,
361 AphrontRequest
$request) {
363 $viewer = $request->getUser();
365 $signature_data = array();
367 switch ($document->getSignatureType()) {
368 case LegalpadDocument
::SIGNATURE_TYPE_NONE
:
370 case LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
:
371 if ($viewer->isLoggedIn()) {
372 $signer_phid = $viewer->getPHID();
373 $signature_data = array(
374 'name' => $viewer->getRealName(),
375 'email' => $viewer->loadPrimaryEmailAddress(),
377 } else if ($request->isFormPost()) {
378 $email = new PhutilEmailAddress($request->getStr('email'));
379 if (strlen($email->getDomainName())) {
380 $email_obj = id(new PhabricatorUserEmail())
381 ->loadOneWhere('address = %s', $email->getAddress());
383 return $this->signInResponse();
388 case LegalpadDocument
::SIGNATURE_TYPE_CORPORATION
:
389 $signer_phid = $viewer->getPHID();
391 $signature_data = array(
392 'contact.name' => $viewer->getRealName(),
393 'email' => $viewer->loadPrimaryEmailAddress(),
394 'actorPHID' => $viewer->getPHID(),
400 return array($signer_phid, $signature_data);
403 private function buildSignatureForm(
404 LegalpadDocument
$document,
405 LegalpadDocumentSignature
$signature,
408 $viewer = $this->getRequest()->getUser();
409 $data = $signature->getSignatureData();
411 $form = id(new AphrontFormView())
414 $signature_type = $document->getSignatureType();
415 switch ($signature_type) {
416 case LegalpadDocument
::SIGNATURE_TYPE_NONE
:
417 // bail out of here quick
419 case LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
:
420 $this->buildIndividualSignatureForm(
426 case LegalpadDocument
::SIGNATURE_TYPE_CORPORATION
:
427 $this->buildCorporateSignatureForm(
436 'This document has an unknown signature type ("%s").',
442 id(new AphrontFormCheckboxControl())
443 ->setError(idx($errors, 'agree', null))
447 pht('I agree to the terms laid forth above.'),
449 if ($document->getRequireSignature()) {
450 $cancel_uri = '/logout/';
451 $cancel_text = pht('Log Out');
453 $cancel_uri = $this->getApplicationURI();
454 $cancel_text = pht('Cancel');
458 id(new AphrontFormSubmitControl())
459 ->setValue(pht('Sign Document'))
460 ->addCancelButton($cancel_uri, $cancel_text));
465 private function buildIndividualSignatureForm(
466 AphrontFormView
$form,
467 LegalpadDocument
$document,
468 LegalpadDocumentSignature
$signature,
471 $data = $signature->getSignatureData();
475 id(new AphrontFormTextControl())
476 ->setLabel(pht('Name'))
477 ->setValue(idx($data, 'name', ''))
479 ->setError(idx($errors, 'name', null)));
481 $viewer = $this->getRequest()->getUser();
482 if (!$viewer->isLoggedIn()) {
484 id(new AphrontFormTextControl())
485 ->setLabel(pht('Email'))
486 ->setValue(idx($data, 'email', ''))
488 ->setError(idx($errors, 'email', null)));
494 private function buildCorporateSignatureForm(
495 AphrontFormView
$form,
496 LegalpadDocument
$document,
497 LegalpadDocumentSignature
$signature,
500 $data = $signature->getSignatureData();
504 id(new AphrontFormTextControl())
505 ->setLabel(pht('Company Name'))
506 ->setValue(idx($data, 'name', ''))
508 ->setError(idx($errors, 'name', null)))
510 id(new AphrontFormTextAreaControl())
511 ->setLabel(pht('Company Address'))
512 ->setValue(idx($data, 'address', ''))
514 ->setError(idx($errors, 'address', null)))
516 id(new AphrontFormTextControl())
517 ->setLabel(pht('Contact Name'))
518 ->setValue(idx($data, 'contact.name', ''))
519 ->setName('contact.name')
520 ->setError(idx($errors, 'contact.name', null)))
522 id(new AphrontFormTextControl())
523 ->setLabel(pht('Contact Email'))
524 ->setValue(idx($data, 'email', ''))
526 ->setError(idx($errors, 'email', null)));
531 private function readSignatureForm(
532 LegalpadDocument
$document,
533 AphrontRequest
$request) {
535 $signature_type = $document->getSignatureType();
536 switch ($signature_type) {
537 case LegalpadDocument
::SIGNATURE_TYPE_INDIVIDUAL
:
538 $result = $this->readIndividualSignatureForm(
542 case LegalpadDocument
::SIGNATURE_TYPE_CORPORATION
:
543 $result = $this->readCorporateSignatureForm(
550 'This document has an unknown signature type ("%s").',
557 private function readIndividualSignatureForm(
558 LegalpadDocument
$document,
559 AphrontRequest
$request) {
561 $signature_data = array();
563 $field_errors = array();
566 $name = $request->getStr('name');
568 if (!strlen($name)) {
569 $field_errors['name'] = pht('Required');
570 $errors[] = pht('Name field is required.');
572 $field_errors['name'] = null;
574 $signature_data['name'] = $name;
576 $viewer = $request->getUser();
577 if ($viewer->isLoggedIn()) {
578 $email = $viewer->loadPrimaryEmailAddress();
580 $email = $request->getStr('email');
583 if (!strlen($email)) {
584 $field_errors['email'] = pht('Required');
585 $errors[] = pht('Email field is required.');
587 $addr_obj = new PhutilEmailAddress($email);
588 $domain = $addr_obj->getDomainName();
590 $field_errors['email'] = pht('Invalid');
591 $errors[] = pht('A valid email is required.');
593 $field_errors['email'] = null;
597 $signature_data['email'] = $email;
599 return array($signature_data, $errors, $field_errors);
602 private function readCorporateSignatureForm(
603 LegalpadDocument
$document,
604 AphrontRequest
$request) {
606 $viewer = $request->getUser();
607 if (!$viewer->isLoggedIn()) {
610 'You can not sign a document on behalf of a corporation unless '.
611 'you are logged in.'));
614 $signature_data = array();
616 $field_errors = array();
618 $name = $request->getStr('name');
620 if (!strlen($name)) {
621 $field_errors['name'] = pht('Required');
622 $errors[] = pht('Company name is required.');
624 $field_errors['name'] = null;
626 $signature_data['name'] = $name;
628 $address = $request->getStr('address');
629 if (!strlen($address)) {
630 $field_errors['address'] = pht('Required');
631 $errors[] = pht('Company address is required.');
633 $field_errors['address'] = null;
635 $signature_data['address'] = $address;
637 $contact_name = $request->getStr('contact.name');
638 if (!strlen($contact_name)) {
639 $field_errors['contact.name'] = pht('Required');
640 $errors[] = pht('Contact name is required.');
642 $field_errors['contact.name'] = null;
644 $signature_data['contact.name'] = $contact_name;
646 $email = $request->getStr('email');
648 if (!strlen($email)) {
649 $field_errors['email'] = pht('Required');
650 $errors[] = pht('Contact email is required.');
652 $addr_obj = new PhutilEmailAddress($email);
653 $domain = $addr_obj->getDomainName();
655 $field_errors['email'] = pht('Invalid');
656 $errors[] = pht('A valid email is required.');
658 $field_errors['email'] = null;
661 $signature_data['email'] = $email;
663 return array($signature_data, $errors, $field_errors);
666 private function sendVerifySignatureEmail(
667 LegalpadDocument
$doc,
668 LegalpadDocumentSignature
$signature) {
670 $signature_data = $signature->getSignatureData();
671 $email = new PhutilEmailAddress($signature_data['email']);
672 $doc_name = $doc->getTitle();
673 $doc_link = PhabricatorEnv
::getProductionURI('/'.$doc->getMonogram());
674 $path = $this->getApplicationURI(sprintf(
676 $signature->getSecretKey()));
677 $link = PhabricatorEnv
::getProductionURI($path);
679 $name = idx($signature_data, 'name');
683 "This email address was used to sign a Legalpad document ".
686 "Please verify you own this email address and accept the ".
687 "agreement by clicking this link:\n\n".
689 "Your signature is not valid until you complete this ".
690 "verification step.\n\nYou can review the document here:\n\n".
693 PlatformSymbols
::getPlatformServerName(),
698 id(new PhabricatorMetaMTAMail())
699 ->addRawTos(array($email->getAddress()))
700 ->setSubject(pht('[Legalpad] Signature Verification'))
701 ->setForceDelivery(true)
703 ->setRelatedPHID($signature->getDocumentPHID())
707 private function signInResponse() {
708 return id(new Aphront403Response())
711 'The email address specified is associated with an account. '.
712 'Please login to that account and sign this document again.'));