Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / legalpad / controller / LegalpadDocumentSignController.php
blob7798a2aa9d04842b4e7ea1918402e41f68a149f4
1 <?php
3 final class LegalpadDocumentSignController extends LegalpadController {
5 private $isSessionGate;
7 public function shouldAllowPublic() {
8 return true;
11 public function shouldAllowLegallyNonCompliantUsers() {
12 return true;
15 public function setIsSessionGate($is_session_gate) {
16 $this->isSessionGate = $is_session_gate;
17 return $this;
20 public function getIsSessionGate() {
21 return $this->isSessionGate;
24 public function handleRequest(AphrontRequest $request) {
25 $viewer = $request->getUser();
27 $document = id(new LegalpadDocumentQuery())
28 ->setViewer($viewer)
29 ->withIDs(array($request->getURIData('id')))
30 ->needDocumentBodies(true)
31 ->executeOne();
32 if (!$document) {
33 return new Aphront404Response();
36 $information = $this->readSignerInformation(
37 $document,
38 $request);
39 if ($information instanceof AphrontResponse) {
40 return $information;
42 list($signer_phid, $signature_data) = $information;
44 $signature = null;
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
51 $has_signed = true;
52 // this is a status UI element
53 $signed_status = null;
54 break;
55 case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
56 if ($signer_phid) {
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))
66 ->executeOne();
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;
77 if (!$signature) {
78 $has_signed = false;
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
86 // anything.
87 if ($viewer->isLoggedIn()) {
88 $signed_status = id(new PHUIInfoView())
89 ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
90 ->setErrors(
91 array(
92 pht('You have not signed this document yet.'),
93 ));
95 } else {
96 $has_signed = true;
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];
107 $signed_text = pht(
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));
112 } else {
113 $signed_text = pht(
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(
124 'name' => true,
125 'email' => true,
126 'agree' => true,
128 $signature->setSignatureData($signature_data);
129 break;
131 case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
132 $signature = id(new LegalpadDocumentSignature())
133 ->setDocumentPHID($document->getPHID())
134 ->setDocumentVersion($document->getVersions());
136 if ($viewer->isLoggedIn()) {
137 $has_signed = false;
139 $signed_status = null;
140 } else {
141 // This just hides the form.
142 $has_signed = true;
144 $login_text = pht(
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(
153 'name' => true,
154 'address' => true,
155 'contact.name' => true,
156 'email' => true,
158 $signature->setSignatureData($signature_data);
159 break;
162 $errors = array();
163 $hisec_token = null;
164 if ($request->isFormOrHisecPost() && !$has_signed) {
165 list($form_data, $errors, $field_errors) = $this->readSignatureForm(
166 $document,
167 $request);
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');
177 if (!$agree) {
178 $errors[] = pht(
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;
185 } else {
186 $verified = LegalpadDocumentSignature::UNVERIFIED;
188 $signature->setVerified($verified);
190 if (!$errors) {
191 // Require MFA to sign legal documents.
192 if ($viewer->isLoggedIn()) {
193 $workflow_key = sprintf(
194 'legalpad.sign(%s)',
195 $document->getPHID());
197 $hisec_token = id(new PhabricatorAuthSessionEngine())
198 ->setWorkflowKey($workflow_key)
199 ->requireHighSecurityToken(
200 $viewer,
201 $request,
202 $document->getURI());
205 $signature->save();
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;
220 } else {
221 $this->sendVerifySignatureEmail(
222 $document,
223 $signature);
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);
235 $engine->addObject(
236 $document_body,
237 LegalpadDocumentBody::MARKUP_FIELD_TEXT);
238 $engine->process();
240 $document_markup = $engine->getOutput(
241 $document_body,
242 LegalpadDocumentBody::MARKUP_FIELD_TEXT);
244 $title = $document_body->getTitle();
246 $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
248 $can_edit = PhabricatorPolicyFilter::hasCapability(
249 $viewer,
250 $document,
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())
263 ->setHeader($title)
264 ->setUser($viewer)
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();
271 if (!$is_gate) {
272 $header->addActionLink(
273 id(new PHUIButtonView())
274 ->setTag('a')
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())
290 ->setUser($viewer)
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')
300 ->setHeader($header)
301 ->appendChild(
302 array(
303 $signed_status,
304 $preamble_box,
305 $document_markup,
308 $signature_box = null;
309 if (!$has_signed) {
310 $error_view = null;
311 if ($errors) {
312 $error_view = id(new PHUIInfoView())
313 ->setErrors($errors);
316 $signature_form = $this->buildSignatureForm(
317 $document,
318 $signature,
319 $field_errors);
321 switch ($document->getSignatureType()) {
322 default:
323 break;
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);
331 if ($error_view) {
332 $box->setInfoView($error_view);
334 $signature_box = phutil_tag_div(
335 'phui-document-view-pro-box plt', $box);
336 break;
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()
350 ->setTitle($title)
351 ->setCrumbs($crumbs)
352 ->setPageObjectPHIDs(array($document->getPHID()))
353 ->appendChild(array(
354 $content,
355 $box,
359 private function readSignerInformation(
360 LegalpadDocument $document,
361 AphrontRequest $request) {
363 $viewer = $request->getUser();
364 $signer_phid = null;
365 $signature_data = array();
367 switch ($document->getSignatureType()) {
368 case LegalpadDocument::SIGNATURE_TYPE_NONE:
369 break;
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());
382 if ($email_obj) {
383 return $this->signInResponse();
387 break;
388 case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
389 $signer_phid = $viewer->getPHID();
390 if ($signer_phid) {
391 $signature_data = array(
392 'contact.name' => $viewer->getRealName(),
393 'email' => $viewer->loadPrimaryEmailAddress(),
394 'actorPHID' => $viewer->getPHID(),
397 break;
400 return array($signer_phid, $signature_data);
403 private function buildSignatureForm(
404 LegalpadDocument $document,
405 LegalpadDocumentSignature $signature,
406 array $errors) {
408 $viewer = $this->getRequest()->getUser();
409 $data = $signature->getSignatureData();
411 $form = id(new AphrontFormView())
412 ->setUser($viewer);
414 $signature_type = $document->getSignatureType();
415 switch ($signature_type) {
416 case LegalpadDocument::SIGNATURE_TYPE_NONE:
417 // bail out of here quick
418 return;
419 case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
420 $this->buildIndividualSignatureForm(
421 $form,
422 $document,
423 $signature,
424 $errors);
425 break;
426 case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
427 $this->buildCorporateSignatureForm(
428 $form,
429 $document,
430 $signature,
431 $errors);
432 break;
433 default:
434 throw new Exception(
435 pht(
436 'This document has an unknown signature type ("%s").',
437 $signature_type));
440 $form
441 ->appendChild(
442 id(new AphrontFormCheckboxControl())
443 ->setError(idx($errors, 'agree', null))
444 ->addCheckbox(
445 'agree',
446 'agree',
447 pht('I agree to the terms laid forth above.'),
448 false));
449 if ($document->getRequireSignature()) {
450 $cancel_uri = '/logout/';
451 $cancel_text = pht('Log Out');
452 } else {
453 $cancel_uri = $this->getApplicationURI();
454 $cancel_text = pht('Cancel');
456 $form
457 ->appendChild(
458 id(new AphrontFormSubmitControl())
459 ->setValue(pht('Sign Document'))
460 ->addCancelButton($cancel_uri, $cancel_text));
462 return $form;
465 private function buildIndividualSignatureForm(
466 AphrontFormView $form,
467 LegalpadDocument $document,
468 LegalpadDocumentSignature $signature,
469 array $errors) {
471 $data = $signature->getSignatureData();
473 $form
474 ->appendChild(
475 id(new AphrontFormTextControl())
476 ->setLabel(pht('Name'))
477 ->setValue(idx($data, 'name', ''))
478 ->setName('name')
479 ->setError(idx($errors, 'name', null)));
481 $viewer = $this->getRequest()->getUser();
482 if (!$viewer->isLoggedIn()) {
483 $form->appendChild(
484 id(new AphrontFormTextControl())
485 ->setLabel(pht('Email'))
486 ->setValue(idx($data, 'email', ''))
487 ->setName('email')
488 ->setError(idx($errors, 'email', null)));
491 return $form;
494 private function buildCorporateSignatureForm(
495 AphrontFormView $form,
496 LegalpadDocument $document,
497 LegalpadDocumentSignature $signature,
498 array $errors) {
500 $data = $signature->getSignatureData();
502 $form
503 ->appendChild(
504 id(new AphrontFormTextControl())
505 ->setLabel(pht('Company Name'))
506 ->setValue(idx($data, 'name', ''))
507 ->setName('name')
508 ->setError(idx($errors, 'name', null)))
509 ->appendChild(
510 id(new AphrontFormTextAreaControl())
511 ->setLabel(pht('Company Address'))
512 ->setValue(idx($data, 'address', ''))
513 ->setName('address')
514 ->setError(idx($errors, 'address', null)))
515 ->appendChild(
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)))
521 ->appendChild(
522 id(new AphrontFormTextControl())
523 ->setLabel(pht('Contact Email'))
524 ->setValue(idx($data, 'email', ''))
525 ->setName('email')
526 ->setError(idx($errors, 'email', null)));
528 return $form;
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(
539 $document,
540 $request);
541 break;
542 case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
543 $result = $this->readCorporateSignatureForm(
544 $document,
545 $request);
546 break;
547 default:
548 throw new Exception(
549 pht(
550 'This document has an unknown signature type ("%s").',
551 $signature_type));
554 return $result;
557 private function readIndividualSignatureForm(
558 LegalpadDocument $document,
559 AphrontRequest $request) {
561 $signature_data = array();
562 $errors = 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.');
571 } else {
572 $field_errors['name'] = null;
574 $signature_data['name'] = $name;
576 $viewer = $request->getUser();
577 if ($viewer->isLoggedIn()) {
578 $email = $viewer->loadPrimaryEmailAddress();
579 } else {
580 $email = $request->getStr('email');
582 $addr_obj = null;
583 if (!strlen($email)) {
584 $field_errors['email'] = pht('Required');
585 $errors[] = pht('Email field is required.');
586 } else {
587 $addr_obj = new PhutilEmailAddress($email);
588 $domain = $addr_obj->getDomainName();
589 if (!$domain) {
590 $field_errors['email'] = pht('Invalid');
591 $errors[] = pht('A valid email is required.');
592 } else {
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()) {
608 throw new Exception(
609 pht(
610 'You can not sign a document on behalf of a corporation unless '.
611 'you are logged in.'));
614 $signature_data = array();
615 $errors = 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.');
623 } else {
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.');
632 } else {
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.');
641 } else {
642 $field_errors['contact.name'] = null;
644 $signature_data['contact.name'] = $contact_name;
646 $email = $request->getStr('email');
647 $addr_obj = null;
648 if (!strlen($email)) {
649 $field_errors['email'] = pht('Required');
650 $errors[] = pht('Contact email is required.');
651 } else {
652 $addr_obj = new PhutilEmailAddress($email);
653 $domain = $addr_obj->getDomainName();
654 if (!$domain) {
655 $field_errors['email'] = pht('Invalid');
656 $errors[] = pht('A valid email is required.');
657 } else {
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(
675 '/verify/%s/',
676 $signature->getSecretKey()));
677 $link = PhabricatorEnv::getProductionURI($path);
679 $name = idx($signature_data, 'name');
681 $body = pht(
682 "%s:\n\n".
683 "This email address was used to sign a Legalpad document ".
684 "in %s:\n\n".
685 " %s\n\n".
686 "Please verify you own this email address and accept the ".
687 "agreement by clicking this link:\n\n".
688 " %s\n\n".
689 "Your signature is not valid until you complete this ".
690 "verification step.\n\nYou can review the document here:\n\n".
691 " %s\n",
692 $name,
693 PlatformSymbols::getPlatformServerName(),
694 $doc_name,
695 $link,
696 $doc_link);
698 id(new PhabricatorMetaMTAMail())
699 ->addRawTos(array($email->getAddress()))
700 ->setSubject(pht('[Legalpad] Signature Verification'))
701 ->setForceDelivery(true)
702 ->setBody($body)
703 ->setRelatedPHID($signature->getDocumentPHID())
704 ->saveAndSend();
707 private function signInResponse() {
708 return id(new Aphront403Response())
709 ->setForbiddenText(
710 pht(
711 'The email address specified is associated with an account. '.
712 'Please login to that account and sign this document again.'));