Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2892408
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
28 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
index 2cab68d7e6..65dc42062e 100644
--- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
+++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php
@@ -1,102 +1,132 @@
<?php
final class PhabricatorOwnersPackageEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'owners.package';
public function getEngineName() {
return pht('Owners Packages');
}
public function getEngineApplicationClass() {
return 'PhabricatorOwnersApplication';
}
protected function newEditableObject() {
return PhabricatorOwnersPackage::initializeNewPackage($this->getViewer());
}
protected function newObjectQuery() {
return id(new PhabricatorOwnersPackageQuery())
->needPaths(true);
}
protected function getObjectCreateTitleText($object) {
return pht('Create New Package');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Package %s', $object->getName());
}
protected function getObjectEditShortText($object) {
return pht('Package %d', $object->getID());
}
protected function getObjectCreateShortText() {
return pht('Create Package');
}
protected function getObjectViewURI($object) {
$id = $object->getID();
return "/owners/package/{$id}/";
}
protected function buildCustomEditFields($object) {
+
+ $paths_help = pht(<<<EOTEXT
+When updating the paths for a package, pass a list of dictionaries like
+this as the `value` for the transaction:
+
+```lang=json, name="Example Paths Value"
+[
+ {
+ "repositoryPHID": "PHID-REPO-1234",
+ "path": "/path/to/directory/",
+ "excluded": false
+ },
+ {
+ "repositoryPHID": "PHID-REPO-1234",
+ "path": "/another/example/path/",
+ "excluded": false
+ }
+]
+```
+
+This transaction will set the paths to the list you provide, overwriting any
+previous paths.
+
+Generally, you will call `owners.search` first to get a list of current paths
+(which are provided in the same format), make changes, then update them by
+applying a transaction of this type.
+EOTEXT
+ );
+
return array(
id(new PhabricatorTextEditField())
->setKey('name')
->setLabel(pht('Name'))
->setDescription(pht('Name of the package.'))
->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_NAME)
->setIsRequired(true)
->setValue($object->getName()),
id(new PhabricatorDatasourceEditField())
->setKey('owners')
->setLabel(pht('Owners'))
->setDescription(pht('Users and projects which own the package.'))
->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_OWNERS)
->setDatasource(new PhabricatorProjectOrUserDatasource())
->setIsCopyable(true)
->setValue($object->getOwnerPHIDs()),
id(new PhabricatorSelectEditField())
->setKey('auditing')
->setLabel(pht('Auditing'))
->setDescription(
pht(
'Automatically trigger audits for commits affecting files in '.
'this package.'))
->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_AUDITING)
->setIsCopyable(true)
->setValue($object->getAuditingEnabled())
->setOptions(
array(
'' => pht('Disabled'),
'1' => pht('Enabled'),
)),
id(new PhabricatorRemarkupEditField())
->setKey('description')
->setLabel(pht('Description'))
->setDescription(pht('Human-readable description of the package.'))
->setTransactionType(
PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION)
->setValue($object->getDescription()),
id(new PhabricatorSelectEditField())
->setKey('status')
->setLabel(pht('Status'))
->setDescription(pht('Archive or enable the package.'))
->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_STATUS)
->setIsConduitOnly(true)
->setValue($object->getStatus())
->setOptions($object->getStatusNameMap()),
id(new PhabricatorConduitEditField())
->setKey('paths.set')
->setLabel(pht('Paths'))
->setDescription(pht('Set paths for this package.'))
->setIsConduitOnly(true)
- ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_PATHS),
+ ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_PATHS)
+ ->setConduitDocumentation($paths_help),
);
}
}
diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
index 344f22b3c4..b5146e6eb3 100644
--- a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
+++ b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php
@@ -1,201 +1,207 @@
<?php
abstract class PhabricatorEditEngineAPIMethod
extends ConduitAPIMethod {
abstract public function newEditEngine();
public function getApplication() {
$engine = $this->newEditEngine();
$class = $engine->getEngineApplicationClass();
return PhabricatorApplication::getByClass($class);
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht('ApplicationEditor methods are highly unstable.');
}
final protected function defineParamTypes() {
return array(
'transactions' => 'list<map<string, wild>>',
'objectIdentifier' => 'optional id|phid|string',
);
}
final protected function defineReturnType() {
return 'map<string, wild>';
}
final protected function execute(ConduitAPIRequest $request) {
$engine = $this->newEditEngine()
->setViewer($request->getUser());
return $engine->buildConduitResponse($request);
}
final public function getMethodDescription() {
$viewer = $this->getViewer();
$engine = $this->newEditEngine()
->setViewer($viewer);
$types = $engine->getConduitEditTypes();
$out = array();
$out[] = pht(<<<EOTEXT
This is a standard **ApplicationEditor** method which allows you to create and
modify objects by applying transactions.
Each transaction applies one change to the object. For example, to create an
object with a specific title or change the title of an existing object you might
start by building a transaction like this:
```lang=json, name=Example Single Transaction
{
"type": "title",
"value": "New Object Title"
}
```
By passing a list of transactions in the `transactions` parameter, you can
apply a sequence of edits. For example, you'll often pass a value like this to
create an object with several field values or apply changes to multiple fields:
```lang=json, name=Example Transaction List
[
{
"type": "title",
"value": "New Object Title"
},
{
"type": "body",
"value": "New body text for the object."
},
{
"type": "projects.add",
"value": ["PHID-PROJ-1111", "PHID-PROJ-2222"]
}
]
```
Exactly which types of edits are available depends on the object you're editing.
Creating Objects
----------------
To create an object, pass a list of `transactions` but leave `objectIdentifier`
empty. This will create a new object with the initial field values you
specify.
Editing Objects
---------------
To edit an object, pass a list of `transactions` and specify an object to
apply them to with `objectIdentifier`. This will apply the changes to the
object.
You may pass an ID (like `123`), PHID (like `PHID-WXYZ-abcdef...`), or
monogram (like `T123`, for objects which have monograms).
Return Type
-----------
WARNING: The structure of the return value from these methods is likely to
change as ApplicationEditor evolves.
Return values look something like this for now:
```lang=json, name=Example Return Value
{
"object": {
"phid": "PHID-XXXX-1111"
},
"transactions": [
{
"phid": "PHID-YYYY-1111",
},
{
"phid": "PHID-YYYY-2222",
}
]
}
```
The `object` key contains information about the object which was created or
edited.
The `transactions` key contains information about the transactions which were
actually applied. For many reasons, the transactions which actually apply may
be greater or fewer in number than the transactions you provided, or may differ
in their nature in other ways.
Edit Types
==========
This API method supports these edit types:
EOTEXT
);
$key = pht('Key');
$summary = pht('Summary');
$description = pht('Description');
$head_type = pht('Type');
$table = array();
$table[] = "| {$key} | {$summary} |";
$table[] = '|--------|----------------|';
foreach ($types as $type) {
$edit_type = $type->getEditType();
$edit_summary = $type->getSummary();
$table[] = "| `{$edit_type}` | {$edit_summary} |";
}
$out[] = implode("\n", $table);
foreach ($types as $type) {
$section = array();
$section[] = pht('Edit Type: %s', $type->getEditType());
$section[] = '---------';
$section[] = null;
$section[] = $type->getDescription();
$section[] = null;
$section[] = pht(
'This edit generates transactions of type `%s` internally.',
$type->getTransactionType());
$section[] = null;
+ $type_documentation = $type->getConduitDocumentation();
+ if ($type_documentation) {
+ $section[] = $type_documentation;
+ $section[] = null;
+ }
+
$type_description = pht(
'Use `%s` to select this edit type.',
$type->getEditType());
$value_type = $type->getValueType();
if (!strlen($value_type)) {
$value_type = '?';
}
$value_description = $type->getValueDescription();
$table = array();
$table[] = "| {$key} | {$head_type} | {$description} |";
$table[] = '|--------|--------------|----------------|';
$table[] = "| `type` | `const` | {$type_description} |";
$table[] = "| `value` | `{$value_type}` | {$value_description} |";
$section[] = implode("\n", $table);
$out[] = implode("\n", $section);
}
$out = implode("\n\n", $out);
return $out;
}
}
diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php
index 77a90f2d03..db69e493a3 100644
--- a/src/applications/transactions/editfield/PhabricatorEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorEditField.php
@@ -1,680 +1,691 @@
<?php
abstract class PhabricatorEditField extends Phobject {
private $key;
private $viewer;
private $label;
private $aliases = array();
private $value;
private $initialValue;
private $hasValue = false;
private $object;
private $transactionType;
private $metadata = array();
private $description;
private $editTypeKey;
private $isRequired;
+ private $conduitDocumentation;
private $commentActionLabel;
private $commentActionValue;
private $hasCommentActionValue;
private $isLocked;
private $isHidden;
private $isPreview;
private $isEditDefaults;
private $isSubmittedForm;
private $controlError;
private $isReorderable = true;
private $isDefaultable = true;
private $isLockable = true;
private $isCopyable = false;
private $isConduitOnly = false;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setAliases(array $aliases) {
$this->aliases = $aliases;
return $this;
}
public function getAliases() {
return $this->aliases;
}
public function setObject($object) {
$this->object = $object;
return $this;
}
public function getObject() {
return $this->object;
}
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setIsLocked($is_locked) {
$this->isLocked = $is_locked;
return $this;
}
public function getIsLocked() {
return $this->isLocked;
}
public function setIsPreview($preview) {
$this->isPreview = $preview;
return $this;
}
public function getIsPreview() {
return $this->isPreview;
}
public function setIsReorderable($is_reorderable) {
$this->isReorderable = $is_reorderable;
return $this;
}
public function getIsReorderable() {
return $this->isReorderable;
}
public function setIsConduitOnly($is_conduit_only) {
$this->isConduitOnly = $is_conduit_only;
return $this;
}
public function getIsConduitOnly() {
return $this->isConduitOnly;
}
public function setIsEditDefaults($is_edit_defaults) {
$this->isEditDefaults = $is_edit_defaults;
return $this;
}
public function getIsEditDefaults() {
return $this->isEditDefaults;
}
public function setIsDefaultable($is_defaultable) {
$this->isDefaultable = $is_defaultable;
return $this;
}
public function getIsDefaultable() {
return $this->isDefaultable;
}
public function setIsLockable($is_lockable) {
$this->isLockable = $is_lockable;
return $this;
}
public function getIsLockable() {
return $this->isLockable;
}
public function setIsHidden($is_hidden) {
$this->isHidden = $is_hidden;
return $this;
}
public function getIsHidden() {
return $this->isHidden;
}
public function setIsCopyable($is_copyable) {
$this->isCopyable = $is_copyable;
return $this;
}
public function getIsCopyable() {
return $this->isCopyable;
}
public function setIsSubmittedForm($is_submitted) {
$this->isSubmittedForm = $is_submitted;
return $this;
}
public function getIsSubmittedForm() {
return $this->isSubmittedForm;
}
public function setIsRequired($is_required) {
$this->isRequired = $is_required;
return $this;
}
public function getIsRequired() {
return $this->isRequired;
}
public function setControlError($control_error) {
$this->controlError = $control_error;
return $this;
}
public function getControlError() {
return $this->controlError;
}
public function setCommentActionLabel($label) {
$this->commentActionLabel = $label;
return $this;
}
public function getCommentActionLabel() {
return $this->commentActionLabel;
}
public function setCommentActionValue($comment_action_value) {
$this->hasCommentActionValue = true;
$this->commentActionValue = $comment_action_value;
return $this;
}
public function getCommentActionValue() {
return $this->commentActionValue;
}
protected function newControl() {
throw new PhutilMethodNotImplementedException();
}
protected function buildControl() {
if ($this->getIsConduitOnly()) {
return null;
}
$control = $this->newControl();
if ($control === null) {
return null;
}
$control
->setValue($this->getValueForControl())
->setName($this->getKey());
if (!$control->getLabel()) {
$control->setLabel($this->getLabel());
}
if ($this->getIsSubmittedForm()) {
$error = $this->getControlError();
if ($error !== null) {
$control->setError($error);
}
} else if ($this->getIsRequired()) {
$control->setError(true);
}
return $control;
}
protected function renderControl() {
$control = $this->buildControl();
if ($control === null) {
return null;
}
if ($this->getIsPreview()) {
$disabled = true;
$hidden = false;
} else if ($this->getIsEditDefaults()) {
$disabled = false;
$hidden = false;
} else {
$disabled = $this->getIsLocked();
$hidden = $this->getIsHidden();
}
if ($hidden) {
return null;
}
$control->setDisabled($disabled);
return $control;
}
public function appendToForm(AphrontFormView $form) {
$control = $this->renderControl();
if ($control !== null) {
if ($this->getIsPreview()) {
if ($this->getIsHidden()) {
$control
->addClass('aphront-form-preview-hidden')
->setError(pht('Hidden'));
} else if ($this->getIsLocked()) {
$control
->setError(pht('Locked'));
}
}
$form->appendControl($control);
}
return $this;
}
protected function getValueForControl() {
return $this->getValue();
}
public function getValueForDefaults() {
$value = $this->getValue();
// By default, just treat the empty string like `null` since they're
// equivalent for almost all fields and this reduces the number of
// meaningless transactions we generate when adjusting defaults.
if ($value === '') {
return null;
}
return $value;
}
protected function getValue() {
return $this->value;
}
public function setValue($value) {
$this->hasValue = true;
$this->initialValue = $value;
$this->value = $value;
return $this;
}
public function setMetadataValue($key, $value) {
$this->metadata[$key] = $value;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function getValueForTransaction() {
return $this->getValue();
}
public function getTransactionType() {
return $this->transactionType;
}
public function setTransactionType($type) {
$this->transactionType = $type;
return $this;
}
public function readValueFromRequest(AphrontRequest $request) {
$check = $this->getAllReadValueFromRequestKeys();
foreach ($check as $key) {
if (!$this->getValueExistsInRequest($request, $key)) {
continue;
}
$this->value = $this->getValueFromRequest($request, $key);
break;
}
return $this;
}
public function readValueFromComment($value) {
$this->value = $this->getValueFromComment($value);
return $this;
}
protected function getValueFromComment($value) {
return $value;
}
public function getAllReadValueFromRequestKeys() {
$keys = array();
$keys[] = $this->getKey();
foreach ($this->getAliases() as $alias) {
$keys[] = $alias;
}
return $keys;
}
public function readDefaultValueFromConfiguration($value) {
$this->value = $this->getDefaultValueFromConfiguration($value);
return $this;
}
protected function getDefaultValueFromConfiguration($value) {
return $value;
}
protected function getValueFromObject($object) {
if ($this->hasValue) {
return $this->value;
} else {
return $this->getDefaultValue();
}
}
protected function getValueExistsInRequest(AphrontRequest $request, $key) {
return $this->getHTTPParameterValueExists($request, $key);
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
return $this->getHTTPParameterValue($request, $key);
}
public function readValueFromField(PhabricatorEditField $other) {
$this->value = $this->getValueFromField($other);
return $this;
}
protected function getValueFromField(PhabricatorEditField $other) {
return $other->getValue();
}
/**
* Read and return the value the object had when the user first loaded the
* form.
*
* This is the initial value from the user's point of view when they started
* the edit process, and used primarily to prevent race conditions for fields
* like "Projects" and "Subscribers" that use tokenizers and support edge
* transactions.
*
* Most fields do not need to store these values or deal with initial value
* handling.
*
* @param AphrontRequest Request to read from.
* @param string Key to read.
* @return wild Value read from request.
*/
protected function getInitialValueFromSubmit(AphrontRequest $request, $key) {
return null;
}
public function getInitialValue() {
return $this->initialValue;
}
public function setInitialValue($initial_value) {
$this->initialValue = $initial_value;
return $this;
}
public function readValueFromSubmit(AphrontRequest $request) {
$key = $this->getKey();
if ($this->getValueExistsInSubmit($request, $key)) {
$value = $this->getValueFromSubmit($request, $key);
} else {
$value = $this->getDefaultValue();
}
$this->value = $value;
$initial_value = $this->getInitialValueFromSubmit($request, $key);
$this->initialValue = $initial_value;
return $this;
}
protected function getValueExistsInSubmit(AphrontRequest $request, $key) {
return $this->getHTTPParameterValueExists($request, $key);
}
protected function getValueFromSubmit(AphrontRequest $request, $key) {
return $this->getHTTPParameterValue($request, $key);
}
protected function getHTTPParameterValueExists(
AphrontRequest $request,
$key) {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getExists($request, $key);
}
return false;
}
protected function getHTTPParameterValue($request, $key) {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getValue($request, $key);
}
return null;
}
protected function getDefaultValue() {
$type = $this->getHTTPParameterType();
if ($type) {
return $type->getDefaultValue();
}
return null;
}
final public function getHTTPParameterType() {
if ($this->getIsConduitOnly()) {
return null;
}
$type = $this->newHTTPParameterType();
if ($type) {
$type->setViewer($this->getViewer());
}
return $type;
}
protected function newHTTPParameterType() {
return new AphrontStringHTTPParameterType();
}
public function setEditTypeKey($edit_type_key) {
$this->editTypeKey = $edit_type_key;
return $this;
}
public function getEditTypeKey() {
if ($this->editTypeKey === null) {
return $this->getKey();
}
return $this->editTypeKey;
}
protected function newEditType() {
// TODO: This could be a little cleaner.
$http_type = $this->getHTTPParameterType();
if ($http_type) {
$value_type = $http_type->getTypeName();
} else {
$value_type = 'wild';
}
return id(new PhabricatorSimpleEditType())
->setValueType($value_type);
}
protected function getEditType() {
$transaction_type = $this->getTransactionType();
if ($transaction_type === null) {
return null;
}
$type_key = $this->getEditTypeKey();
return $this->newEditType()
->setEditType($type_key)
->setTransactionType($transaction_type)
->setDescription($this->getDescription())
- ->setMetadata($this->getMetadata());
+ ->setMetadata($this->getMetadata())
+ ->setConduitDocumentation($this->getConduitDocumentation());
}
public function getConduitEditTypes() {
$edit_type = $this->getEditType();
if ($edit_type === null) {
return array();
}
return array($edit_type);
}
public function getCommentAction() {
$label = $this->getCommentActionLabel();
if ($label === null) {
return null;
}
$action = $this->newCommentAction();
if ($action === null) {
return null;
}
if ($this->hasCommentActionValue) {
$value = $this->getCommentActionValue();
} else {
$value = $this->getValue();
}
$action
->setKey($this->getKey())
->setLabel($label)
->setValue($this->getValueForCommentAction($value));
return $action;
}
protected function newCommentAction() {
return null;
}
protected function getValueForCommentAction($value) {
return $value;
}
public function shouldGenerateTransactionsFromSubmit() {
if ($this->getIsConduitOnly()) {
return false;
}
$edit_type = $this->getEditType();
if (!$edit_type) {
return false;
}
return true;
}
public function shouldReadValueFromRequest() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function shouldReadValueFromSubmit() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function shouldGenerateTransactionsFromComment() {
if ($this->getIsConduitOnly()) {
return false;
}
if ($this->getIsLocked()) {
return false;
}
if ($this->getIsHidden()) {
return false;
}
return true;
}
public function generateTransactions(
PhabricatorApplicationTransaction $template,
array $spec) {
$edit_type = $this->getEditType();
if (!$edit_type) {
throw new Exception(
pht(
'EditField (with key "%s", of class "%s") is generating '.
'transactions, but has no EditType.',
$this->getKey(),
get_class($this)));
}
return $edit_type->generateTransactions($template, $spec);
}
+ public function setConduitDocumentation($conduit_documentation) {
+ $this->conduitDocumentation = $conduit_documentation;
+ return $this;
+ }
+
+ public function getConduitDocumentation() {
+ return $this->conduitDocumentation;
+ }
+
}
diff --git a/src/applications/transactions/edittype/PhabricatorEditType.php b/src/applications/transactions/edittype/PhabricatorEditType.php
index c376046374..b6940f4601 100644
--- a/src/applications/transactions/edittype/PhabricatorEditType.php
+++ b/src/applications/transactions/edittype/PhabricatorEditType.php
@@ -1,99 +1,109 @@
<?php
abstract class PhabricatorEditType extends Phobject {
private $editType;
private $transactionType;
private $label;
private $field;
private $description;
private $summary;
private $metadata = array();
+ private $conduitDocumentation;
public function setDescription($description) {
$this->description = $description;
return $this;
}
public function getDescription() {
return $this->description;
}
public function setSummary($summary) {
$this->summary = $summary;
return $this;
}
public function getSummary() {
if ($this->summary === null) {
return $this->getDescription();
}
return $this->summary;
}
public function setLabel($label) {
$this->label = $label;
return $this;
}
public function getLabel() {
return $this->label;
}
public function setField(PhabricatorEditField $field) {
$this->field = $field;
return $this;
}
public function getField() {
return $this->field;
}
public function setEditType($edit_type) {
$this->editType = $edit_type;
return $this;
}
public function getEditType() {
return $this->editType;
}
+ public function setConduitDocumentation($conduit_documentation) {
+ $this->conduitDocumentation = $conduit_documentation;
+ return $this;
+ }
+
+ public function getConduitDocumentation() {
+ return $this->conduitDocumentation;
+ }
+
public function setMetadata($metadata) {
$this->metadata = $metadata;
return $this;
}
public function getMetadata() {
return $this->metadata;
}
public function setTransactionType($transaction_type) {
$this->transactionType = $transaction_type;
return $this;
}
public function getTransactionType() {
return $this->transactionType;
}
abstract public function generateTransactions(
PhabricatorApplicationTransaction $template,
array $spec);
abstract public function getValueType();
abstract public function getValueDescription();
protected function newTransaction(
PhabricatorApplicationTransaction $template) {
$xaction = id(clone $template)
->setTransactionType($this->getTransactionType());
foreach ($this->getMetadata() as $key => $value) {
$xaction->setMetadataValue($key, $value);
}
return $xaction;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 16:46 (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1126619
Default Alt Text
(28 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment