[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/legalpad/controller/ -> LegalpadDocumentSignController.php (source)

   1  <?php
   2  
   3  final class LegalpadDocumentSignController extends LegalpadController {
   4  
   5    private $id;
   6  
   7    public function shouldAllowPublic() {
   8      return true;
   9    }
  10  
  11    public function willProcessRequest(array $data) {
  12      $this->id = $data['id'];
  13    }
  14  
  15    public function processRequest() {
  16      $request = $this->getRequest();
  17      $viewer = $request->getUser();
  18  
  19      $document = id(new LegalpadDocumentQuery())
  20        ->setViewer($viewer)
  21        ->withIDs(array($this->id))
  22        ->needDocumentBodies(true)
  23        ->executeOne();
  24      if (!$document) {
  25        return new Aphront404Response();
  26      }
  27  
  28      list($signer_phid, $signature_data) = $this->readSignerInformation(
  29        $document,
  30        $request);
  31  
  32      $signature = null;
  33  
  34      $type_individual = LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL;
  35      $is_individual = ($document->getSignatureType() == $type_individual);
  36      if ($is_individual) {
  37        if ($signer_phid) {
  38          // TODO: This is odd and should probably be adjusted after grey/external
  39          // accounts work better, but use the omnipotent viewer to check for a
  40          // signature so we can pick up anonymous/grey signatures.
  41  
  42          $signature = id(new LegalpadDocumentSignatureQuery())
  43            ->setViewer(PhabricatorUser::getOmnipotentUser())
  44            ->withDocumentPHIDs(array($document->getPHID()))
  45            ->withSignerPHIDs(array($signer_phid))
  46            ->executeOne();
  47  
  48          if ($signature && !$viewer->isLoggedIn()) {
  49            return $this->newDialog()
  50              ->setTitle(pht('Already Signed'))
  51              ->appendParagraph(pht('You have already signed this document!'))
  52              ->addCancelButton('/'.$document->getMonogram(), pht('Okay'));
  53          }
  54        }
  55  
  56        $signed_status = null;
  57        if (!$signature) {
  58          $has_signed = false;
  59          $signature = id(new LegalpadDocumentSignature())
  60            ->setSignerPHID($signer_phid)
  61            ->setDocumentPHID($document->getPHID())
  62            ->setDocumentVersion($document->getVersions());
  63  
  64          // If the user is logged in, show a notice that they haven't signed.
  65          // If they aren't logged in, we can't be as sure, so don't show
  66          // anything.
  67          if ($viewer->isLoggedIn()) {
  68            $signed_status = id(new AphrontErrorView())
  69              ->setSeverity(AphrontErrorView::SEVERITY_WARNING)
  70              ->setErrors(
  71                array(
  72                  pht('You have not signed this document yet.'),
  73                ));
  74          }
  75        } else {
  76          $has_signed = true;
  77          $signature_data = $signature->getSignatureData();
  78  
  79          // In this case, we know they've signed.
  80          $signed_at = $signature->getDateCreated();
  81  
  82          if ($signature->getIsExemption()) {
  83            $exemption_phid = $signature->getExemptionPHID();
  84            $handles = $this->loadViewerHandles(array($exemption_phid));
  85            $exemption_handle = $handles[$exemption_phid];
  86  
  87            $signed_text = pht(
  88              'You do not need to sign this document. '.
  89              '%s added a signature exemption for you on %s.',
  90              $exemption_handle->renderLink(),
  91              phabricator_datetime($signed_at, $viewer));
  92          } else {
  93            $signed_text = pht(
  94              'You signed this document on %s.',
  95              phabricator_datetime($signed_at, $viewer));
  96          }
  97  
  98          $signed_status = id(new AphrontErrorView())
  99            ->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
 100            ->setErrors(array($signed_text));
 101        }
 102  
 103        $field_errors = array(
 104          'name' => true,
 105          'email' => true,
 106          'agree' => true,
 107        );
 108      } else {
 109        $signature = id(new LegalpadDocumentSignature())
 110          ->setDocumentPHID($document->getPHID())
 111          ->setDocumentVersion($document->getVersions());
 112  
 113        if ($viewer->isLoggedIn()) {
 114          $has_signed = false;
 115  
 116          $signed_status = null;
 117        } else {
 118          // This just hides the form.
 119          $has_signed = true;
 120  
 121          $login_text = pht(
 122            'This document requires a corporate signatory. You must log in to '.
 123            'accept this document on behalf of a company you represent.');
 124          $signed_status = id(new AphrontErrorView())
 125            ->setSeverity(AphrontErrorView::SEVERITY_WARNING)
 126            ->setErrors(array($login_text));
 127        }
 128  
 129        $field_errors = array(
 130          'name' => true,
 131          'address' => true,
 132          'contact.name' => true,
 133          'email' => true,
 134        );
 135      }
 136  
 137      $signature->setSignatureData($signature_data);
 138  
 139      $errors = array();
 140      if ($request->isFormOrHisecPost() && !$has_signed) {
 141  
 142        // Require two-factor auth to sign legal documents.
 143        if ($viewer->isLoggedIn()) {
 144          $engine = new PhabricatorAuthSessionEngine();
 145          $engine->requireHighSecuritySession(
 146            $viewer,
 147            $request,
 148            '/'.$document->getMonogram());
 149        }
 150  
 151        list($form_data, $errors, $field_errors) = $this->readSignatureForm(
 152          $document,
 153          $request);
 154  
 155        $signature_data = $form_data + $signature_data;
 156  
 157        $signature->setSignatureData($signature_data);
 158        $signature->setSignatureType($document->getSignatureType());
 159        $signature->setSignerName((string)idx($signature_data, 'name'));
 160        $signature->setSignerEmail((string)idx($signature_data, 'email'));
 161  
 162        $agree = $request->getExists('agree');
 163        if (!$agree) {
 164          $errors[] = pht(
 165            'You must check "I agree to the terms laid forth above."');
 166          $field_errors['agree'] = pht('Required');
 167        }
 168  
 169        if ($viewer->isLoggedIn() && $is_individual) {
 170          $verified = LegalpadDocumentSignature::VERIFIED;
 171        } else {
 172          $verified = LegalpadDocumentSignature::UNVERIFIED;
 173        }
 174        $signature->setVerified($verified);
 175  
 176        if (!$errors) {
 177          $signature->save();
 178  
 179          // If the viewer is logged in, send them to the document page, which
 180          // will show that they have signed the document. Otherwise, send them
 181          // to a completion page.
 182          if ($viewer->isLoggedIn() && $is_individual) {
 183            $next_uri = '/'.$document->getMonogram();
 184          } else {
 185            $this->sendVerifySignatureEmail(
 186              $document,
 187              $signature);
 188  
 189            $next_uri = $this->getApplicationURI('done/');
 190          }
 191  
 192          return id(new AphrontRedirectResponse())->setURI($next_uri);
 193        }
 194      }
 195  
 196      $document_body = $document->getDocumentBody();
 197      $engine = id(new PhabricatorMarkupEngine())
 198        ->setViewer($viewer);
 199      $engine->addObject(
 200        $document_body,
 201        LegalpadDocumentBody::MARKUP_FIELD_TEXT);
 202      $engine->process();
 203  
 204      $document_markup = $engine->getOutput(
 205        $document_body,
 206        LegalpadDocumentBody::MARKUP_FIELD_TEXT);
 207  
 208      $title = $document_body->getTitle();
 209  
 210      $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/');
 211  
 212      $can_edit = PhabricatorPolicyFilter::hasCapability(
 213        $viewer,
 214        $document,
 215        PhabricatorPolicyCapability::CAN_EDIT);
 216  
 217      $header = id(new PHUIHeaderView())
 218        ->setHeader($title)
 219        ->addActionLink(
 220          id(new PHUIButtonView())
 221            ->setTag('a')
 222            ->setIcon(
 223              id(new PHUIIconView())
 224                ->setIconFont('fa-pencil'))
 225            ->setText(pht('Manage Document'))
 226            ->setHref($manage_uri)
 227            ->setDisabled(!$can_edit)
 228            ->setWorkflow(!$can_edit));
 229  
 230      $preamble = null;
 231      if (strlen($document->getPreamble())) {
 232        $preamble_text = PhabricatorMarkupEngine::renderOneObject(
 233          id(new PhabricatorMarkupOneOff())->setContent(
 234            $document->getPreamble()),
 235          'default',
 236          $viewer);
 237  
 238        $preamble = id(new PHUIPropertyListView())
 239          ->addSectionHeader(pht('Preamble'))
 240          ->addTextContent($preamble_text);
 241      }
 242  
 243      $content = id(new PHUIDocumentView())
 244        ->addClass('legalpad')
 245        ->setHeader($header)
 246        ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS)
 247        ->appendChild(
 248          array(
 249            $signed_status,
 250            $preamble,
 251            $document_markup,
 252          ));
 253  
 254      if (!$has_signed) {
 255        $error_view = null;
 256        if ($errors) {
 257          $error_view = id(new AphrontErrorView())
 258            ->setErrors($errors);
 259        }
 260  
 261        $signature_form = $this->buildSignatureForm(
 262          $document,
 263          $signature,
 264          $field_errors);
 265  
 266        $subheader = id(new PHUIHeaderView())
 267          ->setHeader(pht('Agree and Sign Document'))
 268          ->setBleedHeader(true);
 269  
 270        $content->appendChild(
 271          array(
 272            $subheader,
 273            $error_view,
 274            $signature_form,
 275          ));
 276      }
 277  
 278      $crumbs = $this->buildApplicationCrumbs();
 279      $crumbs->addTextCrumb($document->getMonogram());
 280  
 281      return $this->buildApplicationPage(
 282        array(
 283          $crumbs,
 284          $content,
 285        ),
 286        array(
 287          'title' => $title,
 288          'pageObjects' => array($document->getPHID()),
 289        ));
 290    }
 291  
 292    private function readSignerInformation(
 293      LegalpadDocument $document,
 294      AphrontRequest $request) {
 295  
 296      $viewer = $request->getUser();
 297      $signer_phid = null;
 298      $signature_data = array();
 299  
 300      switch ($document->getSignatureType()) {
 301        case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
 302          if ($viewer->isLoggedIn()) {
 303            $signer_phid = $viewer->getPHID();
 304            $signature_data = array(
 305              'name' => $viewer->getRealName(),
 306              'email' => $viewer->loadPrimaryEmailAddress(),
 307            );
 308          } else if ($request->isFormPost()) {
 309            $email = new PhutilEmailAddress($request->getStr('email'));
 310            if (strlen($email->getDomainName())) {
 311              $email_obj = id(new PhabricatorUserEmail())
 312                ->loadOneWhere('address = %s', $email->getAddress());
 313              if ($email_obj) {
 314                return $this->signInResponse();
 315              }
 316              $external_account = id(new PhabricatorExternalAccountQuery())
 317                ->setViewer($viewer)
 318                ->withAccountTypes(array('email'))
 319                ->withAccountDomains(array($email->getDomainName()))
 320                ->withAccountIDs(array($email->getAddress()))
 321                ->loadOneOrCreate();
 322              if ($external_account->getUserPHID()) {
 323                return $this->signInResponse();
 324              }
 325              $signer_phid = $external_account->getPHID();
 326            }
 327          }
 328          break;
 329        case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
 330          $signer_phid = $viewer->getPHID();
 331          if ($signer_phid) {
 332            $signature_data = array(
 333              'contact.name' => $viewer->getRealName(),
 334              'email' => $viewer->loadPrimaryEmailAddress(),
 335              'actorPHID' => $viewer->getPHID(),
 336            );
 337          }
 338          break;
 339      }
 340  
 341      return array($signer_phid, $signature_data);
 342    }
 343  
 344    private function buildSignatureForm(
 345      LegalpadDocument $document,
 346      LegalpadDocumentSignature $signature,
 347      array $errors) {
 348  
 349      $viewer = $this->getRequest()->getUser();
 350      $data = $signature->getSignatureData();
 351  
 352      $form = id(new AphrontFormView())
 353        ->setUser($viewer);
 354  
 355      $signature_type = $document->getSignatureType();
 356      switch ($signature_type) {
 357        case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
 358          $this->buildIndividualSignatureForm(
 359            $form,
 360            $document,
 361            $signature,
 362            $errors);
 363          break;
 364        case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
 365          $this->buildCorporateSignatureForm(
 366            $form,
 367            $document,
 368            $signature,
 369            $errors);
 370          break;
 371        default:
 372          throw new Exception(
 373            pht(
 374              'This document has an unknown signature type ("%s").',
 375              $signature_type));
 376      }
 377  
 378      $form
 379        ->appendChild(
 380          id(new AphrontFormCheckboxControl())
 381            ->setError(idx($errors, 'agree', null))
 382            ->addCheckbox(
 383              'agree',
 384              'agree',
 385              pht('I agree to the terms laid forth above.'),
 386              false))
 387        ->appendChild(
 388          id(new AphrontFormSubmitControl())
 389            ->setValue(pht('Sign Document'))
 390            ->addCancelButton($this->getApplicationURI()));
 391  
 392      return $form;
 393    }
 394  
 395    private function buildIndividualSignatureForm(
 396      AphrontFormView $form,
 397      LegalpadDocument $document,
 398      LegalpadDocumentSignature $signature,
 399      array $errors) {
 400  
 401      $data = $signature->getSignatureData();
 402  
 403      $form
 404        ->appendChild(
 405          id(new AphrontFormTextControl())
 406          ->setLabel(pht('Name'))
 407          ->setValue(idx($data, 'name', ''))
 408          ->setName('name')
 409          ->setError(idx($errors, 'name', null)));
 410  
 411      $viewer = $this->getRequest()->getUser();
 412      if (!$viewer->isLoggedIn()) {
 413        $form->appendChild(
 414          id(new AphrontFormTextControl())
 415            ->setLabel(pht('Email'))
 416            ->setValue(idx($data, 'email', ''))
 417            ->setName('email')
 418            ->setError(idx($errors, 'email', null)));
 419      }
 420  
 421      return $form;
 422    }
 423  
 424    private function buildCorporateSignatureForm(
 425      AphrontFormView $form,
 426      LegalpadDocument $document,
 427      LegalpadDocumentSignature $signature,
 428      array $errors) {
 429  
 430      $data = $signature->getSignatureData();
 431  
 432      $form
 433        ->appendChild(
 434          id(new AphrontFormTextControl())
 435          ->setLabel(pht('Company Name'))
 436          ->setValue(idx($data, 'name', ''))
 437          ->setName('name')
 438          ->setError(idx($errors, 'name', null)))
 439        ->appendChild(
 440          id(new AphrontFormTextAreaControl())
 441          ->setLabel(pht('Company Address'))
 442          ->setValue(idx($data, 'address', ''))
 443          ->setName('address')
 444          ->setError(idx($errors, 'address', null)))
 445        ->appendChild(
 446          id(new AphrontFormTextControl())
 447          ->setLabel(pht('Contact Name'))
 448          ->setValue(idx($data, 'contact.name', ''))
 449          ->setName('contact.name')
 450          ->setError(idx($errors, 'contact.name', null)))
 451        ->appendChild(
 452          id(new AphrontFormTextControl())
 453          ->setLabel(pht('Contact Email'))
 454          ->setValue(idx($data, 'email', ''))
 455          ->setName('email')
 456          ->setError(idx($errors, 'email', null)));
 457  
 458      return $form;
 459    }
 460  
 461    private function readSignatureForm(
 462      LegalpadDocument $document,
 463      AphrontRequest $request) {
 464  
 465      $signature_type = $document->getSignatureType();
 466      switch ($signature_type) {
 467        case LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL:
 468          $result = $this->readIndividualSignatureForm(
 469            $document,
 470            $request);
 471          break;
 472        case LegalpadDocument::SIGNATURE_TYPE_CORPORATION:
 473          $result = $this->readCorporateSignatureForm(
 474            $document,
 475            $request);
 476          break;
 477        default:
 478          throw new Exception(
 479            pht(
 480              'This document has an unknown signature type ("%s").',
 481              $signature_type));
 482      }
 483  
 484      return $result;
 485    }
 486  
 487    private function readIndividualSignatureForm(
 488      LegalpadDocument $document,
 489      AphrontRequest $request) {
 490  
 491      $signature_data = array();
 492      $errors = array();
 493      $field_errors = array();
 494  
 495  
 496      $name = $request->getStr('name');
 497  
 498      if (!strlen($name)) {
 499        $field_errors['name'] = pht('Required');
 500        $errors[] = pht('Name field is required.');
 501      } else {
 502        $field_errors['name'] = null;
 503      }
 504      $signature_data['name'] = $name;
 505  
 506      $viewer = $request->getUser();
 507      if ($viewer->isLoggedIn()) {
 508        $email = $viewer->loadPrimaryEmailAddress();
 509      } else {
 510        $email = $request->getStr('email');
 511  
 512        $addr_obj = null;
 513        if (!strlen($email)) {
 514          $field_errors['email'] = pht('Required');
 515          $errors[] = pht('Email field is required.');
 516        } else {
 517          $addr_obj = new PhutilEmailAddress($email);
 518          $domain = $addr_obj->getDomainName();
 519          if (!$domain) {
 520            $field_errors['email'] = pht('Invalid');
 521            $errors[] = pht('A valid email is required.');
 522          } else {
 523            $field_errors['email'] = null;
 524          }
 525        }
 526      }
 527      $signature_data['email'] = $email;
 528  
 529      return array($signature_data, $errors, $field_errors);
 530    }
 531  
 532    private function readCorporateSignatureForm(
 533      LegalpadDocument $document,
 534      AphrontRequest $request) {
 535  
 536      $viewer = $request->getUser();
 537      if (!$viewer->isLoggedIn()) {
 538        throw new Exception(
 539          pht(
 540            'You can not sign a document on behalf of a corporation unless '.
 541            'you are logged in.'));
 542      }
 543  
 544      $signature_data = array();
 545      $errors = array();
 546      $field_errors = array();
 547  
 548      $name = $request->getStr('name');
 549  
 550      if (!strlen($name)) {
 551        $field_errors['name'] = pht('Required');
 552        $errors[] = pht('Company name is required.');
 553      } else {
 554        $field_errors['name'] = null;
 555      }
 556      $signature_data['name'] = $name;
 557  
 558      $address = $request->getStr('address');
 559      if (!strlen($address)) {
 560        $field_errors['address'] = pht('Required');
 561        $errors[] = pht('Company address is required.');
 562      } else {
 563        $field_errors['address'] = null;
 564      }
 565      $signature_data['address'] = $address;
 566  
 567      $contact_name = $request->getStr('contact.name');
 568      if (!strlen($contact_name)) {
 569        $field_errors['contact.name'] = pht('Required');
 570        $errors[] = pht('Contact name is required.');
 571      } else {
 572        $field_errors['contact.name'] = null;
 573      }
 574      $signature_data['contact.name'] = $contact_name;
 575  
 576      $email = $request->getStr('email');
 577      $addr_obj = null;
 578      if (!strlen($email)) {
 579        $field_errors['email'] = pht('Required');
 580        $errors[] = pht('Contact email is required.');
 581      } else {
 582        $addr_obj = new PhutilEmailAddress($email);
 583        $domain = $addr_obj->getDomainName();
 584        if (!$domain) {
 585          $field_errors['email'] = pht('Invalid');
 586          $errors[] = pht('A valid email is required.');
 587        } else {
 588          $field_errors['email'] = null;
 589        }
 590      }
 591      $signature_data['email'] = $email;
 592  
 593      return array($signature_data, $errors, $field_errors);
 594    }
 595  
 596    private function sendVerifySignatureEmail(
 597      LegalpadDocument $doc,
 598      LegalpadDocumentSignature $signature) {
 599  
 600      $signature_data = $signature->getSignatureData();
 601      $email = new PhutilEmailAddress($signature_data['email']);
 602      $doc_name = $doc->getTitle();
 603      $doc_link = PhabricatorEnv::getProductionURI('/'.$doc->getMonogram());
 604      $path = $this->getApplicationURI(sprintf(
 605        '/verify/%s/',
 606        $signature->getSecretKey()));
 607      $link = PhabricatorEnv::getProductionURI($path);
 608  
 609      $name = idx($signature_data, 'name');
 610  
 611      $body = <<<EOBODY
 612  {$name}:
 613  
 614  This email address was used to sign a Legalpad document in Phabricator:
 615  
 616    {$doc_name}
 617  
 618  Please verify you own this email address and accept the agreement by clicking
 619  this link:
 620  
 621    {$link}
 622  
 623  Your signature is not valid until you complete this verification step.
 624  
 625  You can review the document here:
 626  
 627    {$doc_link}
 628  
 629  EOBODY;
 630  
 631      id(new PhabricatorMetaMTAMail())
 632        ->addRawTos(array($email->getAddress()))
 633        ->setSubject(pht('[Legalpad] Signature Verification'))
 634        ->setForceDelivery(true)
 635        ->setBody($body)
 636        ->setRelatedPHID($signature->getDocumentPHID())
 637        ->saveAndSend();
 638    }
 639  
 640    private function signInResponse() {
 641      return id(new Aphront403Response())
 642        ->setForbiddenText(pht(
 643          'The email address specified is associated with an account. '.
 644          'Please login to that account and sign this document again.'));
 645    }
 646  
 647  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1