Path: blob/master/src/applications/legalpad/controller/LegalpadDocumentSignController.php
13464 views
<?php12final class LegalpadDocumentSignController extends LegalpadController {34private $isSessionGate;56public function shouldAllowPublic() {7return true;8}910public function shouldAllowLegallyNonCompliantUsers() {11return true;12}1314public function setIsSessionGate($is_session_gate) {15$this->isSessionGate = $is_session_gate;16return $this;17}1819public function getIsSessionGate() {20return $this->isSessionGate;21}2223public function handleRequest(AphrontRequest $request) {24$viewer = $request->getUser();2526$document = id(new LegalpadDocumentQuery())27->setViewer($viewer)28->withIDs(array($request->getURIData('id')))29->needDocumentBodies(true)30->executeOne();31if (!$document) {32return new Aphront404Response();33}3435$information = $this->readSignerInformation(36$document,37$request);38if ($information instanceof AphrontResponse) {39return $information;40}41list($signer_phid, $signature_data) = $information;4243$signature = null;4445$type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;46$is_individual = ($document->getSignatureType() == $type_individual);47switch ($document->getSignatureType()) {48case LegalpadDocument::SIGNATURE_TYPE_NONE:49// nothing to sign means this should be true50$has_signed = true;51// this is a status UI element52$signed_status = null;53break;54case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:55if ($signer_phid) {56// TODO: This is odd and should probably be adjusted after57// grey/external accounts work better, but use the omnipotent58// viewer to check for a signature so we can pick up59// anonymous/grey signatures.6061$signature = id(new LegalpadDocumentSignatureQuery())62->setViewer(PhabricatorUser::getOmnipotentUser())63->withDocumentPHIDs(array($document->getPHID()))64->withSignerPHIDs(array($signer_phid))65->executeOne();6667if ($signature && !$viewer->isLoggedIn()) {68return $this->newDialog()69->setTitle(pht('Already Signed'))70->appendParagraph(pht('You have already signed this document!'))71->addCancelButton('/'.$document->getMonogram(), pht('Okay'));72}73}7475$signed_status = null;76if (!$signature) {77$has_signed = false;78$signature = id(new LegalpadDocumentSignature())79->setSignerPHID($signer_phid)80->setDocumentPHID($document->getPHID())81->setDocumentVersion($document->getVersions());8283// If the user is logged in, show a notice that they haven't signed.84// If they aren't logged in, we can't be as sure, so don't show85// anything.86if ($viewer->isLoggedIn()) {87$signed_status = id(new PHUIInfoView())88->setSeverity(PHUIInfoView::SEVERITY_WARNING)89->setErrors(90array(91pht('You have not signed this document yet.'),92));93}94} else {95$has_signed = true;96$signature_data = $signature->getSignatureData();9798// In this case, we know they've signed.99$signed_at = $signature->getDateCreated();100101if ($signature->getIsExemption()) {102$exemption_phid = $signature->getExemptionPHID();103$handles = $this->loadViewerHandles(array($exemption_phid));104$exemption_handle = $handles[$exemption_phid];105106$signed_text = pht(107'You do not need to sign this document. '.108'%s added a signature exemption for you on %s.',109$exemption_handle->renderLink(),110phabricator_datetime($signed_at, $viewer));111} else {112$signed_text = pht(113'You signed this document on %s.',114phabricator_datetime($signed_at, $viewer));115}116117$signed_status = id(new PHUIInfoView())118->setSeverity(PHUIInfoView::SEVERITY_NOTICE)119->setErrors(array($signed_text));120}121122$field_errors = array(123'name' => true,124'email' => true,125'agree' => true,126);127$signature->setSignatureData($signature_data);128break;129130case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:131$signature = id(new LegalpadDocumentSignature())132->setDocumentPHID($document->getPHID())133->setDocumentVersion($document->getVersions());134135if ($viewer->isLoggedIn()) {136$has_signed = false;137138$signed_status = null;139} else {140// This just hides the form.141$has_signed = true;142143$login_text = pht(144'This document requires a corporate signatory. You must log in to '.145'accept this document on behalf of a company you represent.');146$signed_status = id(new PHUIInfoView())147->setSeverity(PHUIInfoView::SEVERITY_WARNING)148->setErrors(array($login_text));149}150151$field_errors = array(152'name' => true,153'address' => true,154'contact.name' => true,155'email' => true,156);157$signature->setSignatureData($signature_data);158break;159}160161$errors = array();162$hisec_token = null;163if ($request->isFormOrHisecPost() && !$has_signed) {164list($form_data, $errors, $field_errors) = $this->readSignatureForm(165$document,166$request);167168$signature_data = $form_data + $signature_data;169170$signature->setSignatureData($signature_data);171$signature->setSignatureType($document->getSignatureType());172$signature->setSignerName((string)idx($signature_data, 'name'));173$signature->setSignerEmail((string)idx($signature_data, 'email'));174175$agree = $request->getExists('agree');176if (!$agree) {177$errors[] = pht(178'You must check "I agree to the terms laid forth above."');179$field_errors['agree'] = pht('Required');180}181182if ($viewer->isLoggedIn() && $is_individual) {183$verified = LegalpadDocumentSignature::VERIFIED;184} else {185$verified = LegalpadDocumentSignature::UNVERIFIED;186}187$signature->setVerified($verified);188189if (!$errors) {190// Require MFA to sign legal documents.191if ($viewer->isLoggedIn()) {192$workflow_key = sprintf(193'legalpad.sign(%s)',194$document->getPHID());195196$hisec_token = id(new PhabricatorAuthSessionEngine())197->setWorkflowKey($workflow_key)198->requireHighSecurityToken(199$viewer,200$request,201$document->getURI());202}203204$signature->save();205206// If the viewer is logged in, signing for themselves, send them to207// the document page, which will show that they have signed the208// document. Unless of course they were required to sign the209// document to use Phabricator; in that case try really hard to210// re-direct them to where they wanted to go.211//212// Otherwise, send them to a completion page.213if ($viewer->isLoggedIn() && $is_individual) {214$next_uri = '/'.$document->getMonogram();215if ($document->getRequireSignature()) {216$request_uri = $request->getRequestURI();217$next_uri = (string)$request_uri;218}219} else {220$this->sendVerifySignatureEmail(221$document,222$signature);223224$next_uri = $this->getApplicationURI('done/');225}226227return id(new AphrontRedirectResponse())->setURI($next_uri);228}229}230231$document_body = $document->getDocumentBody();232$engine = id(new PhabricatorMarkupEngine())233->setViewer($viewer);234$engine->addObject(235$document_body,236LegalpadDocumentBody::MARKUP_FIELD_TEXT);237$engine->process();238239$document_markup = $engine->getOutput(240$document_body,241LegalpadDocumentBody::MARKUP_FIELD_TEXT);242243$title = $document_body->getTitle();244245$manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');246247$can_edit = PhabricatorPolicyFilter::hasCapability(248$viewer,249$document,250PhabricatorPolicyCapability::CAN_EDIT);251252// Use the last content update as the modified date. We don't want to253// show that a document like a TOS was "updated" by an incidental change254// to a field like the preamble or privacy settings which does not actually255// affect the content of the agreement.256$content_updated = $document_body->getDateCreated();257258// NOTE: We're avoiding `setPolicyObject()` here so we don't pick up259// extra UI elements that are unnecessary and clutter the signature page.260// These details are available on the "Manage" page.261$header = id(new PHUIHeaderView())262->setHeader($title)263->setUser($viewer)264->setEpoch($content_updated);265266// If we're showing the user this document because it's required to use267// Phabricator and they haven't signed it, don't show the "Manage" button,268// since it won't work.269$is_gate = $this->getIsSessionGate();270if (!$is_gate) {271$header->addActionLink(272id(new PHUIButtonView())273->setTag('a')274->setIcon('fa-pencil')275->setText(pht('Manage'))276->setHref($manage_uri)277->setDisabled(!$can_edit)278->setWorkflow(!$can_edit));279}280281$preamble_box = null;282if (strlen($document->getPreamble())) {283$preamble_text = new PHUIRemarkupView($viewer, $document->getPreamble());284285// NOTE: We're avoiding `setObject()` here so we don't pick up extra UI286// elements like "Subscribers". This information is available on the287// "Manage" page, but just clutters up the "Signature" page.288$preamble = id(new PHUIPropertyListView())289->setUser($viewer)290->addSectionHeader(pht('Preamble'))291->addTextContent($preamble_text);292293$preamble_box = new PHUIPropertyGroupView();294$preamble_box->addPropertyList($preamble);295}296297$content = id(new PHUIDocumentView())298->addClass('legalpad')299->setHeader($header)300->appendChild(301array(302$signed_status,303$preamble_box,304$document_markup,305));306307$signature_box = null;308if (!$has_signed) {309$error_view = null;310if ($errors) {311$error_view = id(new PHUIInfoView())312->setErrors($errors);313}314315$signature_form = $this->buildSignatureForm(316$document,317$signature,318$field_errors);319320switch ($document->getSignatureType()) {321default:322break;323case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:324case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:325$box = id(new PHUIObjectBoxView())326->addClass('document-sign-box')327->setHeaderText(pht('Agree and Sign Document'))328->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)329->setForm($signature_form);330if ($error_view) {331$box->setInfoView($error_view);332}333$signature_box = phutil_tag_div(334'phui-document-view-pro-box plt', $box);335break;336}337338339}340341$crumbs = $this->buildApplicationCrumbs();342$crumbs->setBorder(true);343$crumbs->addTextCrumb($document->getMonogram());344345$box = id(new PHUITwoColumnView())346->setFooter($signature_box);347348return $this->newPage()349->setTitle($title)350->setCrumbs($crumbs)351->setPageObjectPHIDs(array($document->getPHID()))352->appendChild(array(353$content,354$box,355));356}357358private function readSignerInformation(359LegalpadDocument $document,360AphrontRequest $request) {361362$viewer = $request->getUser();363$signer_phid = null;364$signature_data = array();365366switch ($document->getSignatureType()) {367case LegalpadDocument::SIGNATURE_TYPE_NONE:368break;369case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:370if ($viewer->isLoggedIn()) {371$signer_phid = $viewer->getPHID();372$signature_data = array(373'name' => $viewer->getRealName(),374'email' => $viewer->loadPrimaryEmailAddress(),375);376} else if ($request->isFormPost()) {377$email = new PhutilEmailAddress($request->getStr('email'));378if (strlen($email->getDomainName())) {379$email_obj = id(new PhabricatorUserEmail())380->loadOneWhere('address = %s', $email->getAddress());381if ($email_obj) {382return $this->signInResponse();383}384}385}386break;387case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:388$signer_phid = $viewer->getPHID();389if ($signer_phid) {390$signature_data = array(391'contact.name' => $viewer->getRealName(),392'email' => $viewer->loadPrimaryEmailAddress(),393'actorPHID' => $viewer->getPHID(),394);395}396break;397}398399return array($signer_phid, $signature_data);400}401402private function buildSignatureForm(403LegalpadDocument $document,404LegalpadDocumentSignature $signature,405array $errors) {406407$viewer = $this->getRequest()->getUser();408$data = $signature->getSignatureData();409410$form = id(new AphrontFormView())411->setUser($viewer);412413$signature_type = $document->getSignatureType();414switch ($signature_type) {415case LegalpadDocument::SIGNATURE_TYPE_NONE:416// bail out of here quick417return;418case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:419$this->buildIndividualSignatureForm(420$form,421$document,422$signature,423$errors);424break;425case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:426$this->buildCorporateSignatureForm(427$form,428$document,429$signature,430$errors);431break;432default:433throw new Exception(434pht(435'This document has an unknown signature type ("%s").',436$signature_type));437}438439$form440->appendChild(441id(new AphrontFormCheckboxControl())442->setError(idx($errors, 'agree', null))443->addCheckbox(444'agree',445'agree',446pht('I agree to the terms laid forth above.'),447false));448if ($document->getRequireSignature()) {449$cancel_uri = '/logout/';450$cancel_text = pht('Log Out');451} else {452$cancel_uri = $this->getApplicationURI();453$cancel_text = pht('Cancel');454}455$form456->appendChild(457id(new AphrontFormSubmitControl())458->setValue(pht('Sign Document'))459->addCancelButton($cancel_uri, $cancel_text));460461return $form;462}463464private function buildIndividualSignatureForm(465AphrontFormView $form,466LegalpadDocument $document,467LegalpadDocumentSignature $signature,468array $errors) {469470$data = $signature->getSignatureData();471472$form473->appendChild(474id(new AphrontFormTextControl())475->setLabel(pht('Name'))476->setValue(idx($data, 'name', ''))477->setName('name')478->setError(idx($errors, 'name', null)));479480$viewer = $this->getRequest()->getUser();481if (!$viewer->isLoggedIn()) {482$form->appendChild(483id(new AphrontFormTextControl())484->setLabel(pht('Email'))485->setValue(idx($data, 'email', ''))486->setName('email')487->setError(idx($errors, 'email', null)));488}489490return $form;491}492493private function buildCorporateSignatureForm(494AphrontFormView $form,495LegalpadDocument $document,496LegalpadDocumentSignature $signature,497array $errors) {498499$data = $signature->getSignatureData();500501$form502->appendChild(503id(new AphrontFormTextControl())504->setLabel(pht('Company Name'))505->setValue(idx($data, 'name', ''))506->setName('name')507->setError(idx($errors, 'name', null)))508->appendChild(509id(new AphrontFormTextAreaControl())510->setLabel(pht('Company Address'))511->setValue(idx($data, 'address', ''))512->setName('address')513->setError(idx($errors, 'address', null)))514->appendChild(515id(new AphrontFormTextControl())516->setLabel(pht('Contact Name'))517->setValue(idx($data, 'contact.name', ''))518->setName('contact.name')519->setError(idx($errors, 'contact.name', null)))520->appendChild(521id(new AphrontFormTextControl())522->setLabel(pht('Contact Email'))523->setValue(idx($data, 'email', ''))524->setName('email')525->setError(idx($errors, 'email', null)));526527return $form;528}529530private function readSignatureForm(531LegalpadDocument $document,532AphrontRequest $request) {533534$signature_type = $document->getSignatureType();535switch ($signature_type) {536case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:537$result = $this->readIndividualSignatureForm(538$document,539$request);540break;541case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:542$result = $this->readCorporateSignatureForm(543$document,544$request);545break;546default:547throw new Exception(548pht(549'This document has an unknown signature type ("%s").',550$signature_type));551}552553return $result;554}555556private function readIndividualSignatureForm(557LegalpadDocument $document,558AphrontRequest $request) {559560$signature_data = array();561$errors = array();562$field_errors = array();563564565$name = $request->getStr('name');566567if (!strlen($name)) {568$field_errors['name'] = pht('Required');569$errors[] = pht('Name field is required.');570} else {571$field_errors['name'] = null;572}573$signature_data['name'] = $name;574575$viewer = $request->getUser();576if ($viewer->isLoggedIn()) {577$email = $viewer->loadPrimaryEmailAddress();578} else {579$email = $request->getStr('email');580581$addr_obj = null;582if (!strlen($email)) {583$field_errors['email'] = pht('Required');584$errors[] = pht('Email field is required.');585} else {586$addr_obj = new PhutilEmailAddress($email);587$domain = $addr_obj->getDomainName();588if (!$domain) {589$field_errors['email'] = pht('Invalid');590$errors[] = pht('A valid email is required.');591} else {592$field_errors['email'] = null;593}594}595}596$signature_data['email'] = $email;597598return array($signature_data, $errors, $field_errors);599}600601private function readCorporateSignatureForm(602LegalpadDocument $document,603AphrontRequest $request) {604605$viewer = $request->getUser();606if (!$viewer->isLoggedIn()) {607throw new Exception(608pht(609'You can not sign a document on behalf of a corporation unless '.610'you are logged in.'));611}612613$signature_data = array();614$errors = array();615$field_errors = array();616617$name = $request->getStr('name');618619if (!strlen($name)) {620$field_errors['name'] = pht('Required');621$errors[] = pht('Company name is required.');622} else {623$field_errors['name'] = null;624}625$signature_data['name'] = $name;626627$address = $request->getStr('address');628if (!strlen($address)) {629$field_errors['address'] = pht('Required');630$errors[] = pht('Company address is required.');631} else {632$field_errors['address'] = null;633}634$signature_data['address'] = $address;635636$contact_name = $request->getStr('contact.name');637if (!strlen($contact_name)) {638$field_errors['contact.name'] = pht('Required');639$errors[] = pht('Contact name is required.');640} else {641$field_errors['contact.name'] = null;642}643$signature_data['contact.name'] = $contact_name;644645$email = $request->getStr('email');646$addr_obj = null;647if (!strlen($email)) {648$field_errors['email'] = pht('Required');649$errors[] = pht('Contact email is required.');650} else {651$addr_obj = new PhutilEmailAddress($email);652$domain = $addr_obj->getDomainName();653if (!$domain) {654$field_errors['email'] = pht('Invalid');655$errors[] = pht('A valid email is required.');656} else {657$field_errors['email'] = null;658}659}660$signature_data['email'] = $email;661662return array($signature_data, $errors, $field_errors);663}664665private function sendVerifySignatureEmail(666LegalpadDocument $doc,667LegalpadDocumentSignature $signature) {668669$signature_data = $signature->getSignatureData();670$email = new PhutilEmailAddress($signature_data['email']);671$doc_name = $doc->getTitle();672$doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());673$path = $this->getApplicationURI(sprintf(674'/verify/%s/',675$signature->getSecretKey()));676$link = PhabricatorEnv::getProductionURI($path);677678$name = idx($signature_data, 'name');679680$body = pht(681"%s:\n\n".682"This email address was used to sign a Legalpad document ".683"in %s:\n\n".684" %s\n\n".685"Please verify you own this email address and accept the ".686"agreement by clicking this link:\n\n".687" %s\n\n".688"Your signature is not valid until you complete this ".689"verification step.\n\nYou can review the document here:\n\n".690" %s\n",691$name,692PlatformSymbols::getPlatformServerName(),693$doc_name,694$link,695$doc_link);696697id(new PhabricatorMetaMTAMail())698->addRawTos(array($email->getAddress()))699->setSubject(pht('[Legalpad] Signature Verification'))700->setForceDelivery(true)701->setBody($body)702->setRelatedPHID($signature->getDocumentPHID())703->saveAndSend();704}705706private function signInResponse() {707return id(new Aphront403Response())708->setForbiddenText(709pht(710'The email address specified is associated with an account. '.711'Please login to that account and sign this document again.'));712}713714}715716717