Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/legalpad/controller/LegalpadDocumentSignController.php
13464 views
1
<?php
2
3
final class LegalpadDocumentSignController extends LegalpadController {
4
5
private $isSessionGate;
6
7
public function shouldAllowPublic() {
8
return true;
9
}
10
11
public function shouldAllowLegallyNonCompliantUsers() {
12
return true;
13
}
14
15
public function setIsSessionGate($is_session_gate) {
16
$this->isSessionGate = $is_session_gate;
17
return $this;
18
}
19
20
public function getIsSessionGate() {
21
return $this->isSessionGate;
22
}
23
24
public function handleRequest(AphrontRequest $request) {
25
$viewer = $request->getUser();
26
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();
34
}
35
36
$information = $this->readSignerInformation(
37
$document,
38
$request);
39
if ($information instanceof AphrontResponse) {
40
return $information;
41
}
42
list($signer_phid, $signature_data) = $information;
43
44
$signature = null;
45
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.
61
62
$signature = id(new LegalpadDocumentSignatureQuery())
63
->setViewer(PhabricatorUser::getOmnipotentUser())
64
->withDocumentPHIDs(array($document->getPHID()))
65
->withSignerPHIDs(array($signer_phid))
66
->executeOne();
67
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'));
73
}
74
}
75
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());
83
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
));
94
}
95
} else {
96
$has_signed = true;
97
$signature_data = $signature->getSignatureData();
98
99
// In this case, we know they've signed.
100
$signed_at = $signature->getDateCreated();
101
102
if ($signature->getIsExemption()) {
103
$exemption_phid = $signature->getExemptionPHID();
104
$handles = $this->loadViewerHandles(array($exemption_phid));
105
$exemption_handle = $handles[$exemption_phid];
106
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));
116
}
117
118
$signed_status = id(new PHUIInfoView())
119
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
120
->setErrors(array($signed_text));
121
}
122
123
$field_errors = array(
124
'name' => true,
125
'email' => true,
126
'agree' => true,
127
);
128
$signature->setSignatureData($signature_data);
129
break;
130
131
case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
132
$signature = id(new LegalpadDocumentSignature())
133
->setDocumentPHID($document->getPHID())
134
->setDocumentVersion($document->getVersions());
135
136
if ($viewer->isLoggedIn()) {
137
$has_signed = false;
138
139
$signed_status = null;
140
} else {
141
// This just hides the form.
142
$has_signed = true;
143
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));
150
}
151
152
$field_errors = array(
153
'name' => true,
154
'address' => true,
155
'contact.name' => true,
156
'email' => true,
157
);
158
$signature->setSignatureData($signature_data);
159
break;
160
}
161
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);
168
169
$signature_data = $form_data + $signature_data;
170
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'));
175
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');
181
}
182
183
if ($viewer->isLoggedIn() && $is_individual) {
184
$verified = LegalpadDocumentSignature::VERIFIED;
185
} else {
186
$verified = LegalpadDocumentSignature::UNVERIFIED;
187
}
188
$signature->setVerified($verified);
189
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());
196
197
$hisec_token = id(new PhabricatorAuthSessionEngine())
198
->setWorkflowKey($workflow_key)
199
->requireHighSecurityToken(
200
$viewer,
201
$request,
202
$document->getURI());
203
}
204
205
$signature->save();
206
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.
212
//
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;
219
}
220
} else {
221
$this->sendVerifySignatureEmail(
222
$document,
223
$signature);
224
225
$next_uri = $this->getApplicationURI('done/');
226
}
227
228
return id(new AphrontRedirectResponse())->setURI($next_uri);
229
}
230
}
231
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();
239
240
$document_markup = $engine->getOutput(
241
$document_body,
242
LegalpadDocumentBody::MARKUP_FIELD_TEXT);
243
244
$title = $document_body->getTitle();
245
246
$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
247
248
$can_edit = PhabricatorPolicyFilter::hasCapability(
249
$viewer,
250
$document,
251
PhabricatorPolicyCapability::CAN_EDIT);
252
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();
258
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);
266
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));
280
}
281
282
$preamble_box = null;
283
if (strlen($document->getPreamble())) {
284
$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());
285
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);
293
294
$preamble_box = new PHUIPropertyGroupView();
295
$preamble_box->addPropertyList($preamble);
296
}
297
298
$content = id(new PHUIDocumentView())
299
->addClass('legalpad')
300
->setHeader($header)
301
->appendChild(
302
array(
303
$signed_status,
304
$preamble_box,
305
$document_markup,
306
));
307
308
$signature_box = null;
309
if (!$has_signed) {
310
$error_view = null;
311
if ($errors) {
312
$error_view = id(new PHUIInfoView())
313
->setErrors($errors);
314
}
315
316
$signature_form = $this->buildSignatureForm(
317
$document,
318
$signature,
319
$field_errors);
320
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);
333
}
334
$signature_box = phutil_tag_div(
335
'phui-document-view-pro-box plt', $box);
336
break;
337
}
338
339
340
}
341
342
$crumbs = $this->buildApplicationCrumbs();
343
$crumbs->setBorder(true);
344
$crumbs->addTextCrumb($document->getMonogram());
345
346
$box = id(new PHUITwoColumnView())
347
->setFooter($signature_box);
348
349
return $this->newPage()
350
->setTitle($title)
351
->setCrumbs($crumbs)
352
->setPageObjectPHIDs(array($document->getPHID()))
353
->appendChild(array(
354
$content,
355
$box,
356
));
357
}
358
359
private function readSignerInformation(
360
LegalpadDocument $document,
361
AphrontRequest $request) {
362
363
$viewer = $request->getUser();
364
$signer_phid = null;
365
$signature_data = array();
366
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(),
376
);
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();
384
}
385
}
386
}
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(),
395
);
396
}
397
break;
398
}
399
400
return array($signer_phid, $signature_data);
401
}
402
403
private function buildSignatureForm(
404
LegalpadDocument $document,
405
LegalpadDocumentSignature $signature,
406
array $errors) {
407
408
$viewer = $this->getRequest()->getUser();
409
$data = $signature->getSignatureData();
410
411
$form = id(new AphrontFormView())
412
->setUser($viewer);
413
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));
438
}
439
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');
455
}
456
$form
457
->appendChild(
458
id(new AphrontFormSubmitControl())
459
->setValue(pht('Sign Document'))
460
->addCancelButton($cancel_uri, $cancel_text));
461
462
return $form;
463
}
464
465
private function buildIndividualSignatureForm(
466
AphrontFormView $form,
467
LegalpadDocument $document,
468
LegalpadDocumentSignature $signature,
469
array $errors) {
470
471
$data = $signature->getSignatureData();
472
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)));
480
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)));
489
}
490
491
return $form;
492
}
493
494
private function buildCorporateSignatureForm(
495
AphrontFormView $form,
496
LegalpadDocument $document,
497
LegalpadDocumentSignature $signature,
498
array $errors) {
499
500
$data = $signature->getSignatureData();
501
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)));
527
528
return $form;
529
}
530
531
private function readSignatureForm(
532
LegalpadDocument $document,
533
AphrontRequest $request) {
534
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));
552
}
553
554
return $result;
555
}
556
557
private function readIndividualSignatureForm(
558
LegalpadDocument $document,
559
AphrontRequest $request) {
560
561
$signature_data = array();
562
$errors = array();
563
$field_errors = array();
564
565
566
$name = $request->getStr('name');
567
568
if (!strlen($name)) {
569
$field_errors['name'] = pht('Required');
570
$errors[] = pht('Name field is required.');
571
} else {
572
$field_errors['name'] = null;
573
}
574
$signature_data['name'] = $name;
575
576
$viewer = $request->getUser();
577
if ($viewer->isLoggedIn()) {
578
$email = $viewer->loadPrimaryEmailAddress();
579
} else {
580
$email = $request->getStr('email');
581
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;
594
}
595
}
596
}
597
$signature_data['email'] = $email;
598
599
return array($signature_data, $errors, $field_errors);
600
}
601
602
private function readCorporateSignatureForm(
603
LegalpadDocument $document,
604
AphrontRequest $request) {
605
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.'));
612
}
613
614
$signature_data = array();
615
$errors = array();
616
$field_errors = array();
617
618
$name = $request->getStr('name');
619
620
if (!strlen($name)) {
621
$field_errors['name'] = pht('Required');
622
$errors[] = pht('Company name is required.');
623
} else {
624
$field_errors['name'] = null;
625
}
626
$signature_data['name'] = $name;
627
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;
634
}
635
$signature_data['address'] = $address;
636
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;
643
}
644
$signature_data['contact.name'] = $contact_name;
645
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;
659
}
660
}
661
$signature_data['email'] = $email;
662
663
return array($signature_data, $errors, $field_errors);
664
}
665
666
private function sendVerifySignatureEmail(
667
LegalpadDocument $doc,
668
LegalpadDocumentSignature $signature) {
669
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);
678
679
$name = idx($signature_data, 'name');
680
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);
697
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();
705
}
706
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.'));
713
}
714
715
}
716
717