diff --git a/resources/sql/autopatches/20140629.legalsig.1.sql b/resources/sql/autopatches/20140629.legalsig.1.sql new file mode 100644 index 0000000000..5efd1164f4 --- /dev/null +++ b/resources/sql/autopatches/20140629.legalsig.1.sql @@ -0,0 +1,7 @@ +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD signerName VARCHAR(255) NOT NULL COLLATE utf8_general_ci + AFTER signerPHID; + +ALTER TABLE {$NAMESPACE}_legalpad.legalpad_documentsignature + ADD signerEmail VARCHAR(255) NOT NULL COLLATE utf8_general_ci + AFTER signerName; diff --git a/resources/sql/autopatches/20140629.legalsig.2.php b/resources/sql/autopatches/20140629.legalsig.2.php new file mode 100644 index 0000000000..6ded26b9e0 --- /dev/null +++ b/resources/sql/autopatches/20140629.legalsig.2.php @@ -0,0 +1,17 @@ +establishConnection('w'); +foreach (new LiskMigrationIterator($table) as $signature) { + echo pht("Updating Legalpad signature %d...\n", $signature->getID()); + + $data = $signature->getSignatureData(); + + queryfx( + $conn_w, + 'UPDATE %T SET signerName = %s, signerEmail = %s WHERE id = %d', + $table->getTableName(), + (string)idx($data, 'name'), + (string)idx($data, 'email'), + $signature->getID()); +} diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index e06f944425..35ddd7d918 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -1,358 +1,364 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $document = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) ->needDocumentBodies(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $signer_phid = null; $signature_data = array(); if ($viewer->isLoggedIn()) { $signer_phid = $viewer->getPHID(); $signature_data = array( 'name' => $viewer->getRealName(), 'email' => $viewer->loadPrimaryEmailAddress(), ); } else if ($request->isFormPost()) { $email = new PhutilEmailAddress($request->getStr('email')); if (strlen($email->getDomainName())) { $email_obj = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $email->getAddress()); if ($email_obj) { return $this->signInResponse(); } $external_account = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withAccountTypes(array('email')) ->withAccountDomains(array($email->getDomainName())) ->withAccountIDs(array($email->getAddress())) ->loadOneOrCreate(); if ($external_account->getUserPHID()) { return $this->signInResponse(); } $signer_phid = $external_account->getPHID(); } } $signature = null; if ($signer_phid) { // TODO: This is odd and should probably be adjusted after grey/external // accounts work better, but use the omnipotent viewer to check for a // signature so we can pick up anonymous/grey signatures. $signature = id(new LegalpadDocumentSignatureQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDocumentPHIDs(array($document->getPHID())) ->withSignerPHIDs(array($signer_phid)) ->withDocumentVersions(array($document->getVersions())) ->executeOne(); if ($signature && !$viewer->isLoggedIn()) { return $this->newDialog() ->setTitle(pht('Already Signed')) ->appendParagraph(pht('You have already signed this document!')) ->addCancelButton('/'.$document->getMonogram(), pht('Okay')); } } $signed_status = null; if (!$signature) { $has_signed = false; $signature = id(new LegalpadDocumentSignature()) ->setSignerPHID($signer_phid) ->setDocumentPHID($document->getPHID()) ->setDocumentVersion($document->getVersions()) + ->setSignerName((string)idx($signature_data, 'name')) + ->setSignerEmail((string)idx($signature_data, 'email')) ->setSignatureData($signature_data); // If the user is logged in, show a notice that they haven't signed. // If they aren't logged in, we can't be as sure, so don't show anything. if ($viewer->isLoggedIn()) { $signed_status = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_WARNING) ->setErrors( array( pht('You have not signed this document yet.'), )); } } else { $has_signed = true; $signature_data = $signature->getSignatureData(); // In this case, we know they've signed. $signed_at = $signature->getDateCreated(); $signed_status = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors( array( pht( 'You signed this document on %s.', phabricator_datetime($signed_at, $viewer)), )); } $e_name = true; $e_email = true; $e_agree = null; $errors = array(); if ($request->isFormOrHisecPost() && !$has_signed) { // Require two-factor auth to sign legal documents. - $engine = new PhabricatorAuthSessionEngine(); - $engine->requireHighSecuritySession( - $viewer, - $request, - '/'.$document->getMonogram()); + if ($viewer->isLoggedIn()) { + $engine = new PhabricatorAuthSessionEngine(); + $engine->requireHighSecuritySession( + $viewer, + $request, + '/'.$document->getMonogram()); + } $name = $request->getStr('name'); $agree = $request->getExists('agree'); if (!strlen($name)) { $e_name = pht('Required'); $errors[] = pht('Name field is required.'); } else { $e_name = null; } $signature_data['name'] = $name; if ($viewer->isLoggedIn()) { $email = $viewer->loadPrimaryEmailAddress(); } else { $email = $request->getStr('email'); $addr_obj = null; if (!strlen($email)) { $e_email = pht('Required'); $errors[] = pht('Email field is required.'); } else { $addr_obj = new PhutilEmailAddress($email); $domain = $addr_obj->getDomainName(); if (!$domain) { $e_email = pht('Invalid'); $errors[] = pht('A valid email is required.'); } else { $e_email = null; } } } $signature_data['email'] = $email; + $signature->setSignerName((string)idx($signature_data, 'name')); + $signature->setSignerEmail((string)idx($signature_data, 'email')); $signature->setSignatureData($signature_data); if (!$agree) { $errors[] = pht( 'You must check "I agree to the terms laid forth above."'); $e_agree = pht('Required'); } if ($viewer->isLoggedIn()) { $verified = LegalpadDocumentSignature::VERIFIED; } else { $verified = LegalpadDocumentSignature::UNVERIFIED; } $signature->setVerified($verified); if (!$errors) { $signature->save(); // If the viewer is logged in, send them to the document page, which // will show that they have signed the document. Otherwise, send them // to a completion page. if ($viewer->isLoggedIn()) { $next_uri = '/'.$document->getMonogram(); } else { $next_uri = $this->getApplicationURI('done/'); } return id(new AphrontRedirectResponse())->setURI($next_uri); } } $document_body = $document->getDocumentBody(); $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer); $engine->addObject( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $engine->process(); $document_markup = $engine->getOutput( $document_body, LegalpadDocumentBody::MARKUP_FIELD_TEXT); $title = $document_body->getTitle(); $manage_uri = $this->getApplicationURI('view/'.$document->getID().'/'); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $document, PhabricatorPolicyCapability::CAN_EDIT); $header = id(new PHUIHeaderView()) ->setHeader($title) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon( id(new PHUIIconView()) ->setIconFont('fa-pencil')) ->setText(pht('Manage Document')) ->setHref($manage_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $content = id(new PHUIDocumentView()) ->addClass('legalpad') ->setHeader($header) ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) ->appendChild( array( $signed_status, $document_markup, )); if (!$has_signed) { $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setErrors($errors); } $signature_form = $this->buildSignatureForm( $document_body, $signature, $e_name, $e_email, $e_agree); $subheader = id(new PHUIHeaderView()) ->setHeader(pht('Agree and Sign Document')) ->setBleedHeader(true); $content->appendChild( array( $subheader, $error_view, $signature_form, )); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($document->getMonogram()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $title, 'pageObjects' => array($document->getPHID()), )); } private function buildSignatureForm( LegalpadDocumentBody $body, LegalpadDocumentSignature $signature, $e_name, $e_email, $e_agree) { $viewer = $this->getRequest()->getUser(); $data = $signature->getSignatureData(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setValue(idx($data, 'name', '')) ->setName('name') ->setError($e_name)); if (!$viewer->isLoggedIn()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setValue(idx($data, 'email', '')) ->setName('email') ->setError($e_email)); } $form ->appendChild( id(new AphrontFormCheckboxControl()) ->setError($e_agree) ->addCheckbox( 'agree', 'agree', pht('I agree to the terms laid forth above.'), false)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Sign Document')) ->addCancelButton($this->getApplicationURI())); return $form; } private function sendVerifySignatureEmail( LegalpadDocument $doc, LegalpadDocumentSignature $signature) { $signature_data = $signature->getSignatureData(); $email = new PhutilEmailAddress($signature_data['email']); $doc_link = PhabricatorEnv::getProductionURI($doc->getMonogram()); $path = $this->getApplicationURI(sprintf( '/verify/%s/', $signature->getSecretKey())); $link = PhabricatorEnv::getProductionURI($path); $body = <<addRawTos(array($email->getAddress())) ->setSubject(pht('[Legalpad] Signature Verification')) ->setBody($body) ->setRelatedPHID($signature->getDocumentPHID()) ->saveAndSend(); } private function signInResponse() { return id(new Aphront403Response()) ->setForbiddenText(pht( 'The email address specified is associated with an account. '. 'Please login to that account and sign this document again.')); } } diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php index a7dd22d189..9a4ba46776 100644 --- a/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureQuery.php @@ -1,124 +1,150 @@ ids = $ids; return $this; } public function withDocumentPHIDs(array $phids) { $this->documentPHIDs = $phids; return $this; } public function withSignerPHIDs(array $phids) { $this->signerPHIDs = $phids; return $this; } public function withDocumentVersions(array $versions) { $this->documentVersions = $versions; return $this; } public function withSecretKeys(array $keys) { $this->secretKeys = $keys; return $this; } + public function withNameContains($text) { + $this->nameContains = $text; + return $this; + } + + public function withEmailContains($text) { + $this->emailContains = $text; + return $this; + } + protected function loadPage() { $table = new LegalpadDocumentSignature(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $signatures = $table->loadAllFromArray($data); return $signatures; } protected function willFilterPage(array $signatures) { $document_phids = mpull($signatures, 'getDocumentPHID'); $documents = id(new LegalpadDocumentQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($document_phids) ->execute(); $documents = mpull($documents, null, 'getPHID'); foreach ($signatures as $key => $signature) { $document_phid = $signature->getDocumentPHID(); $document = idx($documents, $document_phid); if ($document) { $signature->attachDocument($document); } else { unset($signatures[$key]); } } return $signatures; } protected function buildWhereClause($conn_r) { $where = array(); $where[] = $this->buildPagingClause($conn_r); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->documentPHIDs !== null) { $where[] = qsprintf( $conn_r, 'documentPHID IN (%Ls)', $this->documentPHIDs); } if ($this->signerPHIDs !== null) { $where[] = qsprintf( $conn_r, 'signerPHID IN (%Ls)', $this->signerPHIDs); } if ($this->documentVersions !== null) { $where[] = qsprintf( $conn_r, 'documentVersion IN (%Ld)', $this->documentVersions); } if ($this->secretKeys !== null) { $where[] = qsprintf( $conn_r, 'secretKey IN (%Ls)', $this->secretKeys); } + if ($this->nameContains !== null) { + $where[] = qsprintf( + $conn_r, + 'signerName LIKE %~', + $this->nameContains); + } + + if ($this->emailContains !== null) { + $where[] = qsprintf( + $conn_r, + 'signerEmail LIKE %~', + $this->emailContains); + } + return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorApplicationLegalpad'; } } diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php index 83d91e8930..497ad48535 100644 --- a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php @@ -1,255 +1,281 @@ document = $document; return $this; } public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); $saved->setParameter( 'signerPHIDs', $this->readUsersFromRequest($request, 'signers')); $saved->setParameter( 'documentPHIDs', $this->readPHIDsFromRequest( $request, 'documents', array( PhabricatorLegalpadPHIDTypeDocument::TYPECONST, ))); + $saved->setParameter('nameContains', $request->getStr('nameContains')); + $saved->setParameter('emailContains', $request->getStr('emailContains')); + return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new LegalpadDocumentSignatureQuery()); $signer_phids = $saved->getParameter('signerPHIDs', array()); if ($signer_phids) { $query->withSignerPHIDs($signer_phids); } if ($this->document) { $query->withDocumentPHIDs(array($this->document->getPHID())); } else { $document_phids = $saved->getParameter('documentPHIDs', array()); if ($document_phids) { $query->withDocumentPHIDs($document_phids); } } + $name_contains = $saved->getParameter('nameContains'); + if (strlen($name_contains)) { + $query->withNameContains($name_contains); + } + + $email_contains = $saved->getParameter('emailContains'); + if (strlen($email_contains)) { + $query->withEmailContains($email_contains); + } + return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $document_phids = $saved_query->getParameter('documentPHIDs', array()); $signer_phids = $saved_query->getParameter('signerPHIDs', array()); $phids = array_merge($document_phids, $signer_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); if (!$this->document) { $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/legalpaddocuments/') ->setName('documents') ->setLabel(pht('Documents')) ->setValue(array_select_keys($handles, $document_phids))); } + $name_contains = $saved_query->getParameter('nameContains', ''); + $email_contains = $saved_query->getParameter('emailContains', ''); + $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setDatasource('/typeahead/common/users/') ->setName('signers') ->setLabel(pht('Signers')) - ->setValue(array_select_keys($handles, $signer_phids))); + ->setValue(array_select_keys($handles, $signer_phids))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name Contains')) + ->setName('nameContains') + ->setValue($name_contains)) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel(pht('Email Contains')) + ->setName('emailContains') + ->setValue($email_contains)); } protected function getURI($path) { if ($this->document) { return '/legalpad/signatures/'.$this->document->getID().'/'.$path; } else { return '/legalpad/signatures/'.$path; } } public function getBuiltinQueryNames() { $names = array( 'all' => pht('All Signatures'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function getRequiredHandlePHIDsForResultList( array $signatures, PhabricatorSavedQuery $query) { return array_merge( mpull($signatures, 'getSignerPHID'), mpull($signatures, 'getDocumentPHID')); } protected function renderResultList( array $signatures, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($signatures, 'LegalpadDocumentSignature'); $viewer = $this->requireViewer(); Javelin::initBehavior('phabricator-tooltips'); $sig_good = $this->renderIcon( 'fa-check', null, pht('Verified, Current')); $sig_old = $this->renderIcon( 'fa-clock-o', 'orange', pht('Signed Older Version')); $sig_unverified = $this->renderIcon( 'fa-envelope', 'red', pht('Unverified Email')); id(new PHUIIconView()) ->setIconFont('fa-envelope', 'red') ->addSigil('has-tooltip') ->setMetadata(array('tip' => pht('Unverified Email'))); $rows = array(); foreach ($signatures as $signature) { - $data = $signature->getSignatureData(); - $name = idx($data, 'name'); - $email = idx($data, 'email'); + $name = $signature->getSignerName(); + $email = $signature->getSignerEmail(); $document = $signature->getDocument(); if (!$signature->isVerified()) { $sig_icon = $sig_unverified; } else if ($signature->getDocumentVersion() != $document->getVersions()) { $sig_icon = $sig_old; } else { $sig_icon = $sig_good; } $rows[] = array( $sig_icon, $handles[$document->getPHID()]->renderLink(), $handles[$signature->getSignerPHID()]->renderLink(), $name, phutil_tag( 'a', array( 'href' => 'mailto:'.$email, ), $email), phabricator_datetime($signature->getDateCreated(), $viewer), ); } $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('No signatures match the query.')) ->setHeaders( array( '', pht('Document'), pht('Account'), pht('Name'), pht('Email'), pht('Signed'), )) ->setColumnVisibility( array( true, // Only show the "Document" column if we aren't scoped to a // particular document. !$this->document, )) ->setColumnClasses( array( '', '', '', '', 'wide', 'right', )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Signatures')) ->appendChild($table); if (!$this->document) { $policy_notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->setErrors( array( pht( 'NOTE: You can only see your own signatures and signatures on '. 'documents you have permission to edit.'), )); $box->setErrorView($policy_notice); } return $box; } private function renderIcon($icon, $color, $title) { Javelin::initBehavior('phabricator-tooltips'); return array( id(new PHUIIconView()) ->setIconFont($icon, $color) ->addSigil('has-tooltip') ->setMetadata(array('tip' => $title)), javelin_tag( 'span', array( 'aural' => true, ), $title), ); } } diff --git a/src/applications/legalpad/storage/LegalpadDocumentSignature.php b/src/applications/legalpad/storage/LegalpadDocumentSignature.php index d9f569f45d..eed19be524 100644 --- a/src/applications/legalpad/storage/LegalpadDocumentSignature.php +++ b/src/applications/legalpad/storage/LegalpadDocumentSignature.php @@ -1,73 +1,75 @@ array( 'signatureData' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public function isVerified() { return ($this->getVerified() != self::UNVERIFIED); } public function getDocument() { return $this->assertAttached($this->document); } public function attachDocument(LegalpadDocument $document) { $this->document = $document; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getDocument()->getPolicy( PhabricatorPolicyCapability::CAN_EDIT); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getSignerPHID()); } public function describeAutomaticCapability($capability) { return null; } }