Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/pholio/constants/PholioTransactionType.php b/src/applications/pholio/constants/PholioTransactionType.php
index f540adc586..ddfd1e6a35 100644
--- a/src/applications/pholio/constants/PholioTransactionType.php
+++ b/src/applications/pholio/constants/PholioTransactionType.php
@@ -1,26 +1,27 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PholioTransactionType extends PholioConstants {
const TYPE_NONE = 'none';
const TYPE_NAME = 'name';
const TYPE_DESCRIPTION = 'description';
const TYPE_VIEW_POLICY = 'viewPolicy';
+ const TYPE_SUBSCRIBERS = 'subscribers';
}
diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php
index 609435b46d..8b29c7aec1 100644
--- a/src/applications/pholio/controller/PholioMockViewController.php
+++ b/src/applications/pholio/controller/PholioMockViewController.php
@@ -1,274 +1,332 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group pholio
*/
final class PholioMockViewController extends PholioController {
private $id;
public function willProcessRequest(array $data) {
$this->id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$mock = id(new PholioMockQuery())
->setViewer($user)
->withIDs(array($this->id))
->executeOne();
if (!$mock) {
return new Aphront404Response();
}
$xactions = id(new PholioTransactionQuery())
->withMockIDs(array($mock->getID()))
->execute();
+ $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID(
+ $mock->getPHID());
$phids = array();
$phids[] = $mock->getAuthorPHID();
foreach ($xactions as $xaction) {
$phids[] = $xaction->getAuthorPHID();
+ foreach ($xaction->getRequiredHandlePHIDs() as $hphid) {
+ $phids[] = $hphid;
+ }
+ }
+ foreach ($subscribers as $subscriber) {
+ $phids[] = $subscriber;
}
$this->loadHandles($phids);
$engine = id(new PhabricatorMarkupEngine())
->setViewer($user);
$engine->addObject($mock, PholioMock::MARKUP_FIELD_DESCRIPTION);
foreach ($xactions as $xaction) {
$engine->addObject($xaction, PholioTransaction::MARKUP_FIELD_COMMENT);
}
$engine->process();
$title = 'M'.$mock->getID().' '.$mock->getName();
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$actions = $this->buildActionView($mock);
- $properties = $this->buildPropertyView($mock, $engine);
+ $properties = $this->buildPropertyView($mock, $engine, $subscribers);
$carousel =
'<h1 style="margin: 2em; padding: 1em; border: 1px dashed grey;">'.
'Carousel Goes Here</h1>';
- $comments =
- '<h1 style="margin: 2em; padding: 1em; border: 1px dashed grey;">'.
- 'Comments/Transactions Go Here</h1>';
-
$xaction_view = $this->buildTransactionView($xactions, $engine);
$add_comment = $this->buildAddCommentView($mock);
$content = array(
$header,
$actions,
$properties,
$carousel,
$xaction_view,
$add_comment,
);
return $this->buildApplicationPage(
$content,
array(
'title' => $title,
'device' => true,
));
}
private function buildActionView(PholioMock $mock) {
$user = $this->getRequest()->getUser();
$actions = id(new PhabricatorActionListView())
->setUser($user)
->setObject($mock);
$can_edit = PhabricatorPolicyFilter::hasCapability(
$user,
$mock,
PhabricatorPolicyCapability::CAN_EDIT);
$actions->addAction(
id(new PhabricatorActionView())
->setIcon('edit')
->setName(pht('Edit Mock'))
->setHref($this->getApplicationURI('/edit/'.$mock->getID()))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
return $actions;
}
private function buildPropertyView(
PholioMock $mock,
- PhabricatorMarkupEngine $engine) {
+ PhabricatorMarkupEngine $engine,
+ array $subscribers) {
$user = $this->getRequest()->getUser();
$properties = new PhabricatorPropertyListView();
$properties->addProperty(
pht('Author'),
$this->getHandle($mock->getAuthorPHID())->renderLink());
$properties->addProperty(
pht('Created'),
phabricator_datetime($mock->getDateCreated(), $user));
$descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions(
$user,
$mock);
$properties->addProperty(
pht('Visible To'),
$descriptions[PhabricatorPolicyCapability::CAN_VIEW]);
+ if ($subscribers) {
+ $sub_view = array();
+ foreach ($subscribers as $subscriber) {
+ $sub_view[] = $this->getHandle($subscriber)->renderLink();
+ }
+ $sub_view = implode(', ', $sub_view);
+ } else {
+ $sub_view = '<em>'.pht('None').'</em>';
+ }
+
+ $properties->addProperty(
+ pht('Subscribers'),
+ $sub_view);
+
$properties->addTextContent(
$engine->getOutput($mock, PholioMock::MARKUP_FIELD_DESCRIPTION));
return $properties;
}
private function buildAddCommentView(PholioMock $mock) {
$user = $this->getRequest()->getUser();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$title = $is_serious
? pht('Add Comment')
: pht('History Beckons');
$header = id(new PhabricatorHeaderView())
->setHeader($title);
$action = $is_serious
? pht('Add Comment')
: pht('Answer The Call');
$form = id(new AphrontFormView())
->setUser($user)
->setAction($this->getApplicationURI('/comment/'.$mock->getID().'/'))
->setWorkflow(true)
->setFlexible(true)
->appendChild(
id(new PhabricatorRemarkupControl())
->setName('comment')
->setLabel(pht('Comment')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($action));
return array(
$header,
$form,
);
}
private function buildTransactionView(
array $xactions,
PhabricatorMarkupEngine $engine) {
assert_instances_of($xactions, 'PholioTransaction');
$view = new PhabricatorTimelineView();
foreach ($xactions as $xaction) {
$author = $this->getHandle($xaction->getAuthorPHID());
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
$xaction_visible = true;
$title = null;
+ $type = $xaction->getTransactionType();
- switch ($xaction->getTransactionType()) {
+ switch ($type) {
case PholioTransactionType::TYPE_NONE:
$title = pht(
'%s added a comment.',
$author->renderLink());
break;
case PholioTransactionType::TYPE_NAME:
if ($old === null) {
$xaction_visible = false;
break;
}
$title = pht(
'%s renamed this mock from "%s" to "%s".',
$author->renderLink(),
phutil_escape_html($old),
phutil_escape_html($new));
break;
case PholioTransactionType::TYPE_DESCRIPTION:
if ($old === null) {
$xaction_visible = false;
break;
}
// TODO: Show diff, like Maniphest.
$title = pht(
'%s updated the description of this mock. '.
'The old description was: %s',
$author->renderLink(),
phutil_escape_html($old));
break;
case PholioTransactionType::TYPE_VIEW_POLICY:
if ($old === null) {
$xaction_visible = false;
break;
}
// TODO: Render human-readable.
$title = pht(
'%s changed the visibility of this mock from "%s" to "%s".',
$author->renderLink(),
phutil_escape_html($old),
phutil_escape_html($new));
break;
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ $rem = array_diff($old, $new);
+ $add = array_diff($new, $old);
+
+ $add_l = array();
+ foreach ($add as $phid) {
+ $add_l[] = $this->getHandle($phid)->renderLink();
+ }
+ $add_l = implode(', ', $add_l);
+
+ $rem_l = array();
+ foreach ($rem as $phid) {
+ $rem_l[] = $this->getHandle($phid)->renderLink();
+ }
+ $rem_l = implode(', ', $rem_l);
+
+ if ($add && $rem) {
+ $title = pht(
+ '%s edited subscriber(s), added %d: %s; removed %d: %s.',
+ $author->renderLink(),
+ $add_l,
+ count($add),
+ $rem_l,
+ count($rem));
+ } else if ($add) {
+ $title = pht(
+ '%s added %d subscriber(s): %s.',
+ $author->renderLink(),
+ count($add),
+ $add_l);
+ } else if ($rem) {
+ $title = pht(
+ '%s removed %d subscribers: %s.',
+ $author->renderLink(),
+ count($rem),
+ $rem_l);
+ }
+ break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
if (!$xaction_visible) {
// Some transactions aren't useful to human viewers, like
// the initial transactions which set the mock's name and description.
continue;
}
$event = id(new PhabricatorTimelineEventView())
->setUserHandle($author);
$event->setTitle($title);
if (strlen($xaction->getComment())) {
$event->appendChild(
$engine->getOutput(
$xaction,
PholioTransaction::MARKUP_FIELD_COMMENT));
}
$view->addEvent($event);
}
return $view;
}
}
diff --git a/src/applications/pholio/editor/PholioMockEditor.php b/src/applications/pholio/editor/PholioMockEditor.php
index da6950be62..a1f727a65f 100644
--- a/src/applications/pholio/editor/PholioMockEditor.php
+++ b/src/applications/pholio/editor/PholioMockEditor.php
@@ -1,148 +1,231 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group pholio
*/
final class PholioMockEditor extends PhabricatorEditor {
private $contentSource;
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source;
return $this;
}
public function getContentSource() {
return $this->contentSource;
}
public function applyTransactions(PholioMock $mock, array $xactions) {
assert_instances_of($xactions, 'PholioTransaction');
$actor = $this->requireActor();
if (!$this->contentSource) {
throw new Exception(
"Call setContentSource() before applyTransactions()!");
}
+ $comments = array();
+ foreach ($xactions as $xaction) {
+ if (strlen($xaction->getComment())) {
+ $comments[] = $xaction->getComment();
+ }
+ $type = $xaction->getTransactionType();
+ if ($type == PholioTransactionType::TYPE_DESCRIPTION) {
+ $comments[] = $xaction->getNewValue();
+ }
+ }
+
+ $mentioned_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(
+ $comments);
+
+ if ($mentioned_phids) {
+ if ($mock->getID()) {
+ $old_subs = PhabricatorSubscribersQuery::loadSubscribersForPHID(
+ $mock->getPHID());
+ } else {
+ $old_subs = array();
+ }
+
+ $new_subs = array_merge($old_subs, $mentioned_phids);
+ $xaction = id(new PholioTransaction())
+ ->setTransactionType(PholioTransactionType::TYPE_SUBSCRIBERS)
+ ->setOldValue($old_subs)
+ ->setNewValue($new_subs);
+ array_unshift($xactions, $xaction);
+ }
+
foreach ($xactions as $xaction) {
$xaction->setContentSource($this->contentSource);
$xaction->setAuthorPHID($actor->getPHID());
}
foreach ($xactions as $key => $xaction) {
$has_effect = $this->applyTransaction($mock, $xaction);
if (!$has_effect) {
unset($xactions[$key]);
}
}
if (!$xactions) {
return;
}
$mock->openTransaction();
$mock->save();
foreach ($xactions as $xaction) {
$xaction->setMockID($mock->getID());
$xaction->save();
}
+
+ // Apply ID/PHID-dependent transactions.
+ foreach ($xactions as $xaction) {
+ $type = $xaction->getTransactionType();
+ switch ($type) {
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ $subeditor = id(new PhabricatorSubscriptionsEditor())
+ ->setObject($mock)
+ ->setActor($this->requireActor())
+ ->subscribeExplicit($xaction->getNewValue())
+ ->save();
+ break;
+ }
+ }
+
$mock->saveTransaction();
PholioIndexer::indexMock($mock);
return $this;
}
private function applyTransaction(
PholioMock $mock,
PholioTransaction $xaction) {
$type = $xaction->getTransactionType();
$old = null;
switch ($type) {
case PholioTransactionType::TYPE_NONE:
$old = null;
break;
case PholioTransactionType::TYPE_NAME:
$old = $mock->getName();
break;
case PholioTransactionType::TYPE_DESCRIPTION:
$old = $mock->getDescription();
break;
case PholioTransactionType::TYPE_VIEW_POLICY:
$old = $mock->getViewPolicy();
break;
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ $old = PhabricatorSubscribersQuery::loadSubscribersForPHID(
+ $mock->getPHID());
+ break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
$xaction->setOldValue($old);
- if (!$this->transactionHasEffect($xaction)) {
+ if (!$this->transactionHasEffect($mock, $xaction)) {
return false;
}
switch ($type) {
case PholioTransactionType::TYPE_NONE:
break;
case PholioTransactionType::TYPE_NAME:
$mock->setName($xaction->getNewValue());
break;
case PholioTransactionType::TYPE_DESCRIPTION:
$mock->setDescription($xaction->getNewValue());
break;
case PholioTransactionType::TYPE_VIEW_POLICY:
$mock->setViewPolicy($xaction->getNewValue());
break;
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ // This applies later.
+ break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
return true;
}
- private function transactionHasEffect(PholioTransaction $xaction) {
+ private function transactionHasEffect(
+ PholioMock $mock,
+ PholioTransaction $xaction) {
+
$effect = false;
+ $old = $xaction->getOldValue();
+ $new = $xaction->getNewValue();
+
$type = $xaction->getTransactionType();
switch ($type) {
case PholioTransactionType::TYPE_NONE:
case PholioTransactionType::TYPE_NAME:
case PholioTransactionType::TYPE_DESCRIPTION:
case PholioTransactionType::TYPE_VIEW_POLICY:
- $effect = ($xaction->getOldValue() !== $xaction->getNewValue());
+ $effect = ($old !== $new);
+ break;
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ $old = nonempty($old, array());
+ $old_map = array_fill_keys($old, true);
+ $filtered = $old;
+
+ foreach ($new as $phid) {
+ if ($mock->getAuthorPHID() == $phid) {
+ // The author may not be explicitly subscribed.
+ continue;
+ }
+ if (isset($old_map[$phid])) {
+ // This PHID was already subscribed.
+ continue;
+ }
+ $filtered[] = $phid;
+ }
+
+ $old = array_keys($old_map);
+ $new = array_values($filtered);
+
+ $xaction->setOldValue($old);
+ $xaction->setNewValue($new);
+
+ $effect = ($old !== $new);
break;
default:
throw new Exception("Unknown transaction type '{$type}'!");
}
if (!$effect) {
if (strlen($xaction->getComment())) {
$xaction->setTransactionType(PholioTransactionType::TYPE_NONE);
$effect = true;
}
}
return $effect;
}
}
diff --git a/src/applications/pholio/storage/PholioTransaction.php b/src/applications/pholio/storage/PholioTransaction.php
index a1fd28dfd2..d7f3bbff4e 100644
--- a/src/applications/pholio/storage/PholioTransaction.php
+++ b/src/applications/pholio/storage/PholioTransaction.php
@@ -1,111 +1,122 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group pholio
*/
final class PholioTransaction extends PholioDAO
implements PhabricatorMarkupInterface {
const MARKUP_FIELD_COMMENT = 'markup:comment';
protected $mockID;
protected $authorPHID;
protected $transactionType;
protected $oldValue;
protected $newValue;
protected $comment = '';
protected $metadata = array();
protected $contentSource;
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'oldValue' => self::SERIALIZATION_JSON,
'newValue' => self::SERIALIZATION_JSON,
'metadata' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setContentSource(PhabricatorContentSource $content_source) {
$this->contentSource = $content_source->serialize();
return $this;
}
public function getContentSource() {
return PhabricatorContentSource::newFromSerialized($this->contentSource);
}
+ public function getRequiredHandlePHIDs() {
+ switch ($this->getTransactionType()) {
+ case PholioTransactionType::TYPE_SUBSCRIBERS:
+ return array_merge(
+ $this->getOldValue(),
+ $this->getNewValue());
+ default:
+ return array();
+ }
+ }
+
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapbility($capability, PhabricatorUser $viewer) {
return ($viewer->getPHID() == $this->getAuthorPHID());
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public function getMarkupFieldKey($field) {
return 'MX:'.$this->getID();
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::newMarkupEngine(array());
}
public function getMarkupText($field) {
return $this->getComment();
}
public function didMarkupText($field, $output, PhutilMarkupEngine $engine) {
return $output;
}
public function shouldUseMarkupCache($field) {
return (bool)$this->getID();
}
}
diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
index 80bd0c4239..c17121add6 100644
--- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
+++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php
@@ -1,90 +1,91 @@
<?php
final class PhabricatorSubscriptionsEditController
extends PhabricatorController {
private $phid;
private $action;
public function willProcessRequest(array $data) {
$this->phid = idx($data, 'phid');
$this->action = idx($data, 'action');
}
public function processRequest() {
$request = $this->getRequest();
if (!$request->isFormPost()) {
return new Aphront400Response();
}
switch ($this->action) {
case 'add':
$is_add = true;
break;
case 'delete':
$is_add = false;
break;
default:
return new Aphront400Response();
}
$user = $request->getUser();
$phid = $this->phid;
// TODO: This is a policy test because `loadObjects()` is not currently
// policy-aware. Once it is, we can collapse this.
$handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user);
if (!$handle->isComplete()) {
return new Aphront404Response();
}
$objects = id(new PhabricatorObjectHandleData(array($phid)))
+ ->setViewer($user)
->loadObjects();
$object = idx($objects, $phid);
if (!($object instanceof PhabricatorSubscribableInterface)) {
return $this->buildErrorResponse(
pht('Bad Object'),
pht('This object is not subscribable.'),
$handle->getURI());
}
if ($object->isAutomaticallySubscribed($user->getPHID())) {
return $this->buildErrorResponse(
pht('Automatically Subscribed'),
pht('You are automatically subscribed to this object.'),
$handle->getURI());
}
$editor = id(new PhabricatorSubscriptionsEditor())
->setActor($user)
->setObject($object);
if ($is_add) {
$editor->subscribeExplicit(array($user->getPHID()), $explicit = true);
} else {
$editor->unsubscribe(array($user->getPHID()));
}
$editor->save();
// TODO: We should just render the "Unsubscribe" action and swap it out
// in the document for Ajax requests.
return id(new AphrontReloadResponse())->setURI($handle->getURI());
}
private function buildErrorResponse($title, $message, $uri) {
$request = $this->getRequest();
$user = $request->getUser();
$dialog = id(new AphrontDialogView())
->setUser($user)
->setTitle($title)
->appendChild($message)
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}
diff --git a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
index c3a78a6900..0870fe27ee 100644
--- a/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
+++ b/src/applications/subscriptions/query/PhabricatorSubscribersQuery.php
@@ -1,50 +1,54 @@
<?php
final class PhabricatorSubscribersQuery extends PhabricatorQuery {
private $objectPHIDs;
private $subscriberPHIDs;
public static function loadSubscribersForPHID($phid) {
+ if (!$phid) {
+ return array();
+ }
+
$subscribers = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs(array($phid))
->execute();
return $subscribers[$phid];
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withSubscriberPHIDs(array $subscriber_phids) {
$this->subscriberPHIDs = $subscriber_phids;
return $this;
}
public function execute() {
$query = new PhabricatorEdgeQuery();
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER;
$query->withSourcePHIDs($this->objectPHIDs);
$query->withEdgeTypes(array($edge_type));
if ($this->subscriberPHIDs) {
$query->withDestinationPHIDs($this->subscriberPHIDs);
}
$edges = $query->execute();
$results = array_fill_keys($this->objectPHIDs, array());
foreach ($edges as $src => $edge_types) {
foreach ($edge_types[$edge_type] as $dst => $data) {
$results[$src][] = $dst;
}
}
return $results;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Aug 14, 10:17 PM (2 d, 17 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196207
Default Alt Text
(26 KB)

Event Timeline