Page MenuHomestyx hydra

No OneTemporary

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 @@
+<?php
+
+$table = new LegalpadDocumentSignature();
+$conn_w = $table->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 @@
<?php
final class LegalpadDocumentSignController extends LegalpadController {
private $id;
public function shouldRequireLogin() {
return false;
}
public function willProcessRequest(array $data) {
$this->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 = <<<EOBODY
Hi {$signature_data['name']},
This email address was used to sign a Legalpad document ({$doc_link}).
Please verify you own this email address by clicking this link:
{$link}
Your signature is invalid until you verify you own the email.
EOBODY;
id(new PhabricatorMetaMTAMail())
->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 @@
<?php
final class LegalpadDocumentSignatureQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $documentPHIDs;
private $signerPHIDs;
private $documentVersions;
private $secretKeys;
+ private $nameContains;
+ private $emailContains;
public function withIDs(array $ids) {
$this->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 @@
<?php
final class LegalpadDocumentSignatureSearchEngine
extends PhabricatorApplicationSearchEngine {
private $document;
public function getResultTypeDescription() {
return pht('Legalpad Signatures');
}
public function getApplicationClassName() {
return 'PhabricatorApplicationLegalpad';
}
public function setDocument(LegalpadDocument $document) {
$this->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 @@
<?php
final class LegalpadDocumentSignature
extends LegalpadDAO
implements PhabricatorPolicyInterface {
const VERIFIED = 0;
const UNVERIFIED = 1;
protected $documentPHID;
protected $documentVersion;
protected $signerPHID;
+ protected $signerName;
+ protected $signerEmail;
protected $signatureData = array();
protected $verified;
protected $secretKey;
private $document = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => 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;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 7:42 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187435
Default Alt Text
(26 KB)

Event Timeline