Page MenuHomePhabricator

D9375.id24727.diff
No OneTemporary

D9375.id24727.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -7,7 +7,7 @@
*/
return array(
'names' => array(
- 'core.pkg.css' => 'c0818fbb',
+ 'core.pkg.css' => 'a5ff7d87',
'core.pkg.js' => '14887b3d',
'darkconsole.pkg.js' => 'df001cab',
'differential.pkg.css' => '4a93db37',
@@ -37,7 +37,7 @@
'rsrc/css/aphront/typeahead.css' => 'a989b5b3',
'rsrc/css/application/auth/auth.css' => '1e655982',
'rsrc/css/application/base/main-menu-view.css' => 'aceca0e9',
- 'rsrc/css/application/base/notification-menu.css' => '8ae4a008',
+ 'rsrc/css/application/base/notification-menu.css' => '5e3b5c86',
'rsrc/css/application/base/phabricator-application-launch-view.css' => '8b7e271d',
'rsrc/css/application/base/standard-page-view.css' => '517cdfb1',
'rsrc/css/application/chatlog/chatlog.css' => '852140ff',
@@ -730,7 +730,7 @@
'phabricator-nav-view-css' => '9283c2df',
'phabricator-notification' => '0c6946e7',
'phabricator-notification-css' => 'ef2c9b34',
- 'phabricator-notification-menu-css' => '8ae4a008',
+ 'phabricator-notification-menu-css' => '5e3b5c86',
'phabricator-object-selector-css' => '029a133d',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => 'bbae734c',
diff --git a/resources/sql/stopwords.txt b/resources/sql/stopwords.txt
new file mode 100644
--- /dev/null
+++ b/resources/sql/stopwords.txt
@@ -0,0 +1,50 @@
+the
+be
+and
+of
+a
+in
+to
+have
+to
+it
+I
+that
+for
+you
+he
+with
+on
+do
+say
+this
+they
+at
+but
+we
+his
+from
+that
+not
+by
+or
+as
+what
+go
+their
+can
+who
+get
+if
+would
+all
+my
+will
+as
+up
+there
+so
+its
+us
+in
+on
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1728,6 +1728,7 @@
'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php',
'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php',
'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php',
+ 'PhabricatorMetaMTAMailSection' => 'applications/metamta/view/PhabricatorMetaMTAMailSection.php',
'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php',
'PhabricatorMetaMTAMailableDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php',
'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php',
diff --git a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
--- a/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
+++ b/src/applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php
@@ -15,6 +15,8 @@
$console = PhutilConsole::getConsole();
$console->getServer()->setEnableLog(true);
+ PhabricatorLDAPAuthProvider::assertLDAPExtensionInstalled();
+
$provider = PhabricatorLDAPAuthProvider::getLDAPProvider();
if (!$provider) {
$console->writeOut(
diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
--- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php
@@ -251,12 +251,26 @@
return array($errors, $issues, $values);
}
+ public static function assertLDAPExtensionInstalled() {
+ if (!function_exists('ldap_bind')) {
+ throw new Exception(
+ pht(
+ 'Before you can set up or use LDAP, you need to install the PHP '.
+ 'LDAP extension. It is not currently installed, so PHP can not '.
+ 'talk to LDAP. Usually you can install it with '.
+ '`yum install php-ldap`, `apt-get install php5-ldap`, or a '.
+ 'similar package manager command.'));
+ }
+ }
+
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
+ self::assertLDAPExtensionInstalled();
+
$labels = $this->getPropertyLabels();
$captions = array(
diff --git a/src/applications/config/check/PhabricatorSetupCheckMySQL.php b/src/applications/config/check/PhabricatorSetupCheckMySQL.php
--- a/src/applications/config/check/PhabricatorSetupCheckMySQL.php
+++ b/src/applications/config/check/PhabricatorSetupCheckMySQL.php
@@ -59,6 +59,106 @@
->setSummary($summary)
->setMessage($message);
}
+
+ $stopword_file = queryfx_one($conn_raw, 'SELECT @@ft_stopword_file');
+ $stopword_file = $stopword_file['@@ft_stopword_file'];
+ if ($stopword_file == '(built-in)') {
+ if (!PhabricatorDefaultSearchEngineSelector::shouldUseElasticSearch()) {
+
+ $root = dirname(phutil_get_library_root('phabricator'));
+ $stopword_path = $root.'/resources/sql/stopwords.txt';
+ $stopword_path = Filesystem::resolvePath($stopword_path);
+
+ $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
+
+ $summary = pht(
+ 'MySQL is using a default stopword file, which will prevent '.
+ 'searching for many common words.');
+
+ $message = pht(
+ "Your MySQL instance is using the builtin stopword file for ".
+ "building search indexes. This can make Phabricator's search ".
+ "feature less useful.\n\n".
+ "Stopwords are common words which are not indexed and thus can not ".
+ "be searched for. The default stopword file has about 500 words, ".
+ "including various words which you are likely to wish to search ".
+ "for, such as 'various', 'likely', 'wish', and 'zero'.\n\n".
+ "To make search more useful, you can use an alternate stopword ".
+ "file with fewer words. Alternatively, if you aren't concerned ".
+ "about searching for common words, you can ignore this warning. ".
+ "If you later plan to configure ElasticSearch, you can also ignore ".
+ "this warning: this stopword file only affects MySQL fulltext ".
+ "indexes.\n\n".
+ "To choose a different stopword file, add this to your %s file ".
+ "(in the %s section) and then restart %s:\n\n".
+ "%s\n".
+ "(You can also use a different file if you prefer. The file ".
+ "suggested above has about 50 of the most common English words.)\n\n".
+ "Finally, run this command to rebuild indexes using the new ".
+ "rules:\n\n".
+ "%s",
+ phutil_tag('tt', array(), 'my.cnf'),
+ phutil_tag('tt', array(), '[mysqld]'),
+ phutil_tag('tt', array(), 'mysqld'),
+ phutil_tag('pre', array(), 'ft_stopword_file='.$stopword_path),
+ phutil_tag(
+ 'pre',
+ array(),
+ "mysql> REPAIR TABLE {$namespace}_search.search_documentfield;"));
+
+ $this->newIssue('mysql.ft_stopword_file')
+ ->setName(pht('MySQL is Using Default Stopword File'))
+ ->setSummary($summary)
+ ->setMessage($message);
+ }
+ }
+
+
+ $min_len = queryfx_one($conn_raw, 'SELECT @@ft_min_word_len');
+ $min_len = $min_len['@@ft_min_word_len'];
+ if ($min_len == 4) {
+ if (!PhabricatorDefaultSearchEngineSelector::shouldUseElasticSearch()) {
+ $namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
+
+ $summary = pht(
+ 'MySQL is configured to only index words with at least 4 '.
+ 'characters.');
+
+ $message = pht(
+ "Your MySQL instance is configured to use the default minimum word ".
+ "length when building search indexes, which is 4. This means words ".
+ "which are only 3 characters long will not be indexed and can not ".
+ "be searched for.\n\n".
+ "For example, you will not be able to find search results for words ".
+ "like 'SMS', 'web', or 'DOS'.\n\n".
+ "You can change this setting to 3 to allow these words to be ".
+ "indexed. Alternatively, you can ignore this warning if you are ".
+ "not concerned about searching for 3-letter words. If you later ".
+ "plan to configure ElasticSearch, you can also ignore this warning: ".
+ "only MySQL fulltext search is affected.\n\n".
+ "To reduce the minimum word length to 3, add this to your %s file ".
+ "(in the %s section) and then restart %s:\n\n".
+ "%s\n".
+ "Finally, run this command to rebuild indexes using the new ".
+ "rules:\n\n".
+ "%s",
+ phutil_tag('tt', array(), 'my.cnf'),
+ phutil_tag('tt', array(), '[mysqld]'),
+ phutil_tag('tt', array(), 'mysqld'),
+ phutil_tag('pre', array(), 'ft_min_word_len=3'),
+ phutil_tag(
+ 'pre',
+ array(),
+ "mysql> REPAIR TABLE {$namespace}_search.search_documentfield;"));
+
+ $this->newIssue('mysql.ft_min_word_len')
+ ->setName(pht('MySQL is Using Default Minimum Word Length'))
+ ->setSummary($summary)
+ ->setMessage($message);
+ }
+ }
+
+
}
}
diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
--- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
+++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php
@@ -331,7 +331,20 @@
'in bytes.'))
->setSummary(pht('Global cap for size of generated emails (bytes).'))
->addExample(524288, pht('Truncate at 512KB'))
- ->addExample(1048576, pht('Truncate at 1MB'))
+ ->addExample(1048576, pht('Truncate at 1MB')),
+ $this->newOption('metamta.html-emails', 'bool', true)
+ ->setBoolOptions(
+ array(
+ pht('Enable HTML emails'),
+ pht('Plaintext emails only'),
+ ))
+ ->setSummary(
+ pht(
+ 'Phabricator can send prettier emails when HTML is enabled '))
+ ->setDescription(
+ pht('HTML emails offer prettier emails (colorized diffs, links etc)'.
+ ', however it may reduce the reliability of reply-by-email')),
+
);
}
diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php
--- a/src/applications/differential/editor/DifferentialTransactionEditor.php
+++ b/src/applications/differential/editor/DifferentialTransactionEditor.php
@@ -1179,20 +1179,21 @@
$config_attach = PhabricatorEnv::getEnvConfig($config_key_attach);
if ($config_inline || $config_attach) {
- $patch = $this->renderPatchForMail($diff);
- $lines = count(phutil_split_lines($patch));
+ $patch_section = $this->renderPatchForMail($diff);
+ $lines = count(phutil_split_lines($patch_section->getPlaintext()));
if ($config_inline && ($lines <= $config_inline)) {
$body->addTextSection(
pht('CHANGE DETAILS'),
- $patch);
+ $patch_section);
}
if ($config_attach) {
$name = pht('D%s.%s.patch', $object->getID(), $diff->getID());
$mime_type = 'text/x-patch; charset=utf-8';
$body->addAttachment(
- new PhabricatorMetaMTAAttachment($patch, $name, $mime_type));
+ new PhabricatorMetaMTAAttachment(
+ $patch_section->getPlaintext(), $name, $mime_type));
}
}
}
@@ -1330,7 +1331,7 @@
$hunk_parser = new DifferentialHunkParser();
}
- $result = array();
+ $section = new PhabricatorMetaMTAMailSection();
foreach ($inline_groups as $changeset_id => $group) {
$changeset = idx($changesets, $changeset_id);
if (!$changeset) {
@@ -1351,25 +1352,27 @@
$inline_content = $comment->getContent();
if (!$show_context) {
- $result[] = "{$file}:{$range} {$inline_content}";
+ $section->addFragment("{$file}:{$range} {$inline_content}");
} else {
- $result[] = '================';
- $result[] = 'Comment at: '.$file.':'.$range;
- $result[] = $hunk_parser->makeContextDiff(
+ $patch = $hunk_parser->makeContextDiff(
$changeset->getHunks(),
$comment->getIsNewFile(),
$comment->getLineNumber(),
$comment->getLineLength(),
1);
- $result[] = '----------------';
- $result[] = $inline_content;
- $result[] = null;
+ $section->addFragment('================')
+ ->addFragment('Comment at: '.$file.':'.$range)
+ ->addPlaintextFragment($patch)
+ ->addHTMLFragment($this->renderPatchHTMLForMail($patch))
+ ->addFragment('----------------')
+ ->addFragment($inline_content)
+ ->addFragment(null);
}
}
}
- return implode("\n", $result);
+ return $section;
}
private function loadDiff($phid, $need_changesets = false) {
@@ -1762,14 +1765,25 @@
return implode("\n", $filenames);
}
+ private function renderPatchHTMLForMail($patch) {
+ return phutil_tag('pre',
+ array('style' => 'font-family: monospace;'), $patch);
+ }
+
private function renderPatchForMail(DifferentialDiff $diff) {
$format = PhabricatorEnv::getEnvConfig('metamta.differential.patch-format');
- return id(new DifferentialRawDiffRenderer())
+ $patch = id(new DifferentialRawDiffRenderer())
->setViewer($this->getActor())
->setFormat($format)
->setChangesets($diff->getChangesets())
->buildPatch();
+
+ $section = new PhabricatorMetaMTAMailSection();
+ $section->addHTMLFragment($this->renderPatchHTMLForMail($patch));
+ $section->addPlaintextFragment($patch);
+
+ return $section;
}
}
diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php
--- a/src/applications/differential/parser/DifferentialChangesetParser.php
+++ b/src/applications/differential/parser/DifferentialChangesetParser.php
@@ -690,22 +690,6 @@
return false;
}
- $old = $changeset->getOldProperties();
- $new = $changeset->getNewProperties();
-
- if ($old === $new) {
- return false;
- }
-
- if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
- $new == array('unix:filemode' => '100644')) {
- return false;
- }
-
- if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
- $old == array('unix:filemode' => '100644')) {
- return false;
- }
return true;
}
@@ -927,6 +911,9 @@
}
}
+ $renderer->attachOldFile($old);
+ $renderer->attachNewFile($new);
+
return $renderer->renderFileChange($old, $new, $id, $vs);
case DifferentialChangeType::FILE_DIRECTORY:
case DifferentialChangeType::FILE_BINARY:
diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
--- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php
@@ -289,13 +289,23 @@
protected function renderPropertyChangeHeader() {
$changeset = $this->getChangeset();
+ list($old, $new) = $this->getChangesetProperties($changeset);
- $old = $changeset->getOldProperties();
- $new = $changeset->getNewProperties();
+ // If we don't have any property changes, don't render this table.
+ if ($old === $new) {
+ return null;
+ }
$keys = array_keys($old + $new);
sort($keys);
+ $key_map = array(
+ 'unix:filemode' => pht('File Mode'),
+ 'file:dimensions' => pht('Image Dimensions'),
+ 'file:mimetype' => pht('MIME Type'),
+ 'file:size' => pht('File Size'),
+ );
+
$rows = array();
foreach ($keys as $key) {
$oval = idx($old, $key);
@@ -313,8 +323,10 @@
$nval = phutil_escape_html_newlines($nval);
}
+ $readable_key = idx($key_map, $key, $key);
+
$rows[] = phutil_tag('tr', array(), array(
- phutil_tag('th', array(), $key),
+ phutil_tag('th', array(), $readable_key),
phutil_tag('td', array('class' => 'oval'), $oval),
phutil_tag('td', array('class' => 'nval'), $nval),
));
diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php
--- a/src/applications/differential/render/DifferentialChangesetRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetRenderer.php
@@ -30,6 +30,9 @@
private $depths;
private $originalCharacterEncoding;
+ private $oldFile = false;
+ private $newFile = false;
+
public function setOriginalCharacterEncoding($original_character_encoding) {
$this->originalCharacterEncoding = $original_character_encoding;
return $this;
@@ -63,6 +66,38 @@
return $this->gaps;
}
+ public function attachOldFile(PhabricatorFile $old = null) {
+ $this->oldFile = $old;
+ return $this;
+ }
+
+ public function getOldFile() {
+ if ($this->oldFile === false) {
+ throw new PhabricatorDataNotAttachedException($this);
+ }
+ return $this->oldFile;
+ }
+
+ public function hasOldFile() {
+ return (bool)$this->oldFile;
+ }
+
+ public function attachNewFile(PhabricatorFile $new = null) {
+ $this->newFile = $new;
+ return $this;
+ }
+
+ public function getNewFile() {
+ if ($this->newFile === false) {
+ throw new PhabricatorDataNotAttachedException($this);
+ }
+ return $this->newFile;
+ }
+
+ public function hasNewFile() {
+ return (bool)$this->newFile;
+ }
+
public function setOriginalNew($original_new) {
$this->originalNew = $original_new;
return $this;
@@ -498,4 +533,43 @@
return $out;
}
+ protected function getChangesetProperties($changeset) {
+ $old = $changeset->getOldProperties();
+ $new = $changeset->getNewProperties();
+
+ // When adding files, don't show the uninteresting 644 filemode change.
+ if ($changeset->getChangeType() == DifferentialChangeType::TYPE_ADD &&
+ $new == array('unix:filemode' => '100644')) {
+ unset($new['unix:filemode']);
+ }
+
+ // Likewise when removing files.
+ if ($changeset->getChangeType() == DifferentialChangeType::TYPE_DELETE &&
+ $old == array('unix:filemode' => '100644')) {
+ unset($old['unix:filemode']);
+ }
+
+ if ($this->hasOldFile()) {
+ $file = $this->getOldFile();
+ if ($file->getImageWidth()) {
+ $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
+ $old['file:dimensions'] = $dimensions;
+ }
+ $old['file:mimetype'] = $file->getMimeType();
+ $old['file:size'] = phutil_format_bytes($file->getByteSize());
+ }
+
+ if ($this->hasNewFile()) {
+ $file = $this->getNewFile();
+ if ($file->getImageWidth()) {
+ $dimensions = $file->getImageWidth().'x'.$file->getImageHeight();
+ $new['file:dimensions'] = $dimensions;
+ }
+ $new['file:mimetype'] = $file->getMimeType();
+ $new['file:size'] = phutil_format_bytes($file->getByteSize());
+ }
+
+ return array($old, $new);
+ }
+
}
diff --git a/src/applications/differential/render/DifferentialChangesetTestRenderer.php b/src/applications/differential/render/DifferentialChangesetTestRenderer.php
--- a/src/applications/differential/render/DifferentialChangesetTestRenderer.php
+++ b/src/applications/differential/render/DifferentialChangesetTestRenderer.php
@@ -23,8 +23,7 @@
protected function renderPropertyChangeHeader() {
$changeset = $this->getChangeset();
- $old = $changeset->getOldProperties();
- $new = $changeset->getNewProperties();
+ list($old, $new) = $this->getChangesetProperties($changeset);
if (!$old && !$new) {
return null;
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
@@ -8,9 +8,9 @@
abstract public function addCCs(array $emails);
abstract public function addAttachment($data, $filename, $mimetype);
abstract public function addHeader($header_name, $header_value);
- abstract public function setBody($body);
+ abstract public function setBody($plaintext_body);
+ abstract public function setHTMLBody($html_body);
abstract public function setSubject($subject);
- abstract public function setIsHTML($is_html);
/**
* Some mailers, notably Amazon SES, do not support us setting a specific
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
@@ -57,13 +57,13 @@
return $this;
}
- public function setSubject($subject) {
- $this->params['subject'] = $subject;
+ public function setHTMLBody($html_body) {
+ $this->params['html-body'] = $html_body;
return $this;
}
- public function setIsHTML($is_html) {
- $this->params['is-html'] = $is_html;
+ public function setSubject($subject) {
+ $this->params['subject'] = $subject;
return $this;
}
@@ -78,11 +78,10 @@
$params['to'] = implode(', ', idx($this->params, 'tos', array()));
$params['subject'] = idx($this->params, 'subject');
+ $params['text'] = idx($this->params, 'body');
- if (idx($this->params, 'is-html')) {
- $params['html'] = idx($this->params, 'body');
- } else {
- $params['text'] = idx($this->params, 'body');
+ if (idx($this->params, 'html-body')) {
+ $params['html'] = idx($this->params, 'html-body');
}
$from = idx($this->params, 'from');
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
@@ -91,17 +91,19 @@
}
public function setBody($body) {
+ $this->mailer->IsHTML(false);
$this->mailer->Body = $body;
return $this;
}
- public function setSubject($subject) {
- $this->mailer->Subject = $subject;
+ public function setHTMLBody($html_body) {
+ $this->mailer->IsHTML(true);
+ $this->mailer->Body = $html_body;
return $this;
}
- public function setIsHTML($is_html) {
- $this->mailer->IsHTML($is_html);
+ public function setSubject($subject) {
+ $this->mailer->Subject = $subject;
return $this;
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
@@ -70,16 +70,24 @@
public function setBody($body) {
$this->mailer->Body = $body;
+ $this->mailer->IsHTML(false);
return $this;
}
- public function setSubject($subject) {
- $this->mailer->Subject = $subject;
+
+ /**
+ * Note: phpmailer-lite does NOT support sending messages with mixed version
+ * (plaintext and html). So for now lets just use HTML if it's available.
+ * @param $html
+ */
+ public function setHTMLBody($html_body) {
+ $this->mailer->Body = $html_body;
+ $this->mailer->IsHTML(true);
return $this;
}
- public function setIsHTML($is_html) {
- $this->mailer->IsHTML($is_html);
+ public function setSubject($subject) {
+ $this->mailer->Subject = $subject;
return $this;
}
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
@@ -56,13 +56,14 @@
return $this;
}
- public function setSubject($subject) {
- $this->params['subject'] = $subject;
+ public function setHTMLBody($body) {
+ $this->params['html-body'] = $body;
return $this;
}
- public function setIsHTML($is_html) {
- $this->params['is-html'] = $is_html;
+
+ public function setSubject($subject) {
+ $this->params['subject'] = $subject;
return $this;
}
@@ -89,10 +90,10 @@
}
$params['subject'] = idx($this->params, 'subject');
- if (idx($this->params, 'is-html')) {
- $params['html'] = idx($this->params, 'body');
- } else {
- $params['text'] = idx($this->params, 'body');
+ $params['text'] = idx($this->params, 'body');
+
+ if (idx($this->params, 'html-body')) {
+ $params['html'] = idx($this->params, 'html-body');
}
$params['from'] = idx($this->params, 'from');
diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
--- a/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
+++ b/src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
@@ -64,13 +64,13 @@
return $this;
}
- public function setSubject($subject) {
- $this->guts['subject'] = $subject;
+ public function setHTMLBody($html_body) {
+ $this->guts['html-body'] = $html_body;
return $this;
}
- public function setIsHTML($is_html) {
- $this->guts['is-html'] = $is_html;
+ public function setSubject($subject) {
+ $this->guts['subject'] = $subject;
return $this;
}
diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
--- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php
@@ -105,7 +105,6 @@
$subject = $args->getArg('subject');
$tags = $args->getArg('tag');
$attach = $args->getArg('attach');
- $is_html = $args->getArg('html');
$is_bulk = $args->getArg('bulk');
$console->writeErr("%s\n", pht('Reading message body from stdin...'));
@@ -117,10 +116,19 @@
->addCCs($ccs)
->setSubject($subject)
->setBody($body)
- ->setIsHTML($is_html)
->setIsBulk($is_bulk)
->setMailTags($tags);
+ if ($args->getArg('html')) {
+ $mail->setBody(
+ pht('(This is a placeholder plaintext email body for a test message '.
+ 'sent with --html.)'));
+
+ $mail->setHTMLBody($body);
+ } else {
+ $mail->setBody($body);
+ }
+
if ($from) {
$mail->setFrom($from->getPHID());
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -212,13 +212,13 @@
return $this;
}
- public function getBody() {
- return $this->getParam('body');
+ public function setHTMLBody($html) {
+ $this->setParam('html-body', $html);
+ return $this;
}
- public function setIsHTML($html) {
- $this->setParam('is-html', $html);
- return $this;
+ public function getBody() {
+ return $this->getParam('body');
}
public function setIsErrorEmail($is_error) {
@@ -377,6 +377,32 @@
$add_cc = array();
$add_to = array();
+ // Only try to use preferences if everything is multiplexed, so we
+ // get consistent behavior.
+ $use_prefs = self::shouldMultiplexAllMail();
+
+ $prefs = null;
+ if ($use_prefs) {
+
+ // If multiplexing is enabled, some recipients will be in "Cc"
+ // rather than "To". We'll move them to "To" later (or supply a
+ // dummy "To") but need to look for the recipient in either the
+ // "To" or "Cc" fields here.
+ $target_phid = head(idx($params, 'to', array()));
+ if (!$target_phid) {
+ $target_phid = head(idx($params, 'cc', array()));
+ }
+
+ if ($target_phid) {
+ $user = id(new PhabricatorUser())->loadOneWhere(
+ 'phid = %s',
+ $target_phid);
+ if ($user) {
+ $prefs = $user->loadPreferences();
+ }
+ }
+ }
+
foreach ($params as $key => $value) {
switch ($key) {
case 'from':
@@ -444,42 +470,7 @@
$attachment->getMimeType());
}
break;
- case 'body':
- $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
- if (strlen($value) > $max) {
- $value = phutil_utf8_shorten($value, $max);
- $value .= "\n";
- $value .= pht('(This email was truncated at %d bytes.)', $max);
- }
- $mailer->setBody($value);
- break;
case 'subject':
- // Only try to use preferences if everything is multiplexed, so we
- // get consistent behavior.
- $use_prefs = self::shouldMultiplexAllMail();
-
- $prefs = null;
- if ($use_prefs) {
-
- // If multiplexing is enabled, some recipients will be in "Cc"
- // rather than "To". We'll move them to "To" later (or supply a
- // dummy "To") but need to look for the recipient in either the
- // "To" or "Cc" fields here.
- $target_phid = head(idx($params, 'to', array()));
- if (!$target_phid) {
- $target_phid = head(idx($params, 'cc', array()));
- }
-
- if ($target_phid) {
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'phid = %s',
- $target_phid);
- if ($user) {
- $prefs = $user->loadPreferences();
- }
- }
- }
-
$subject = array();
if ($is_threaded) {
@@ -518,11 +509,6 @@
$mailer->setSubject(implode(' ', array_filter($subject)));
break;
- case 'is-html':
- if ($value) {
- $mailer->setIsHTML(true);
- }
- break;
case 'is-bulk':
if ($value) {
if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) {
@@ -570,6 +556,26 @@
}
}
+ $body = idx($params, 'body', '');
+ $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
+ if (strlen($body) > $max) {
+ $body = phutil_utf8_shorten($body, $max);
+ $body .= "\n";
+ $body .= pht('(This email was truncated at %d bytes.)', $max);
+ }
+ $mailer->setBody($body);
+
+ $html_emails = PhabricatorEnv::getEnvConfig('metamta.html-emails');
+ if ($use_prefs && $prefs) {
+ $html_emails = $prefs->getPreference(
+ PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS,
+ $html_emails);
+ }
+
+ if ($html_emails && isset($params['html-body'])) {
+ $mailer->setHTMLBody($params['html-body']);
+ }
+
if (!$add_to && !$add_cc) {
$this->setStatus(self::STATUS_VOID);
$this->setMessage(
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
--- a/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailBody.php
@@ -9,6 +9,7 @@
final class PhabricatorMetaMTAMailBody {
private $sections = array();
+ private $htmlSections = array();
private $attachments = array();
@@ -24,7 +25,25 @@
*/
public function addRawSection($text) {
if (strlen($text)) {
- $this->sections[] = rtrim($text);
+ $text = rtrim($text);
+ $this->sections[] = $text;
+ $this->htmlSections[] = phutil_escape_html_newlines(
+ phutil_tag('div', array(), $text));
+ }
+ return $this;
+ }
+
+ public function addRawPlaintextSection($text) {
+ if (strlen($text)) {
+ $text = rtrim($text);
+ $this->sections[] = $text;
+ }
+ return $this;
+ }
+
+ public function addRawHTMLSection($html) {
+ if (strlen($html)) {
+ $this->htmlSections[] = $html;
}
return $this;
}
@@ -41,11 +60,34 @@
* @return this
* @task compose
*/
- public function addTextSection($header, $text) {
+ public function addTextSection($header, $section) {
+ if ($section instanceof PhabricatorMetaMTAMailSection) {
+ $plaintext = $section->getPlaintext();
+ $html = $section->getHTML();
+ } else {
+ $plaintext = $section;
+ $html = phutil_escape_html_newlines(phutil_tag('div', array(), $section));
+ }
+
+ $this->addPlaintextSection($header, $plaintext);
+ $this->addHTMLSection($header, $html);
+ return $this;
+ }
+
+ public function addPlaintextSection($header, $text) {
$this->sections[] = $header."\n".$this->indent($text);
return $this;
}
+ public function addHTMLSection($header, $html_fragment) {
+ $this->htmlSections[] = phutil_tag('div',
+ array(
+ 'style' => 'font-weight:800;'
+ ),
+ $header).$html_fragment;
+
+ return $this;
+ }
/**
* Add a Herald section with a rule management URI and a transcript URI.
@@ -114,6 +156,9 @@
return implode("\n\n", $this->sections)."\n";
}
+ public function renderHTML() {
+ return implode('<br><br>', $this->htmlSections).'<br>';
+ }
/**
* Retrieve attachments.
diff --git a/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php b/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php
new file mode 100644
--- /dev/null
+++ b/src/applications/metamta/view/PhabricatorMetaMTAMailSection.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Helper for building a rendered section.
+ *
+ * @task compose Composition
+ * @task render Rendering
+ * @group metamta
+ */
+
+final class PhabricatorMetaMTAMailSection {
+ private $plaintextFragments = array();
+ private $htmlFragments = array();
+
+ public function getHTML() {
+ return implode('', $this->htmlFragments);
+ }
+
+ public function getPlaintext() {
+ return implode("\n", $this->plaintextFragments);
+ }
+
+ public function addHTMLFragment($fragment) {
+ $this->htmlFragments[] = $fragment;
+ return $this;
+
+ }
+
+ public function addPlaintextFragment($fragment) {
+ $this->plaintextFragments[] = $fragment;
+ return $this;
+ }
+
+ public function addFragment($fragment) {
+ $this->plaintextFragments[] = $fragment;
+ $this->htmlFragments[] =
+ phutil_escape_html_newlines(phutil_tag('div', array(), $fragment));
+
+ return $this;
+ }
+}
diff --git a/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php b/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
--- a/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
+++ b/src/applications/search/selector/PhabricatorDefaultSearchEngineSelector.php
@@ -4,11 +4,16 @@
extends PhabricatorSearchEngineSelector {
public function newEngine() {
- $elastic_host = PhabricatorEnv::getEnvConfig('search.elastic.host');
- if ($elastic_host) {
+ if (self::shouldUseElasticSearch()) {
+ $elastic_host = PhabricatorEnv::getEnvConfig('search.elastic.host');
$elastic_index = PhabricatorEnv::getEnvConfig('search.elastic.namespace');
return new PhabricatorSearchEngineElastic($elastic_host, $elastic_index);
}
return new PhabricatorSearchEngineMySQL();
}
+
+ public static function shouldUseElasticSearch() {
+ return (bool)PhabricatorEnv::getEnvConfig('search.elastic.host');
+ }
+
}
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelEmailFormat.php b/src/applications/settings/panel/PhabricatorSettingsPanelEmailFormat.php
--- a/src/applications/settings/panel/PhabricatorSettingsPanelEmailFormat.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelEmailFormat.php
@@ -22,6 +22,7 @@
$pref_re_prefix = PhabricatorUserPreferences::PREFERENCE_RE_PREFIX;
$pref_vary = PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT;
+ $prefs_html_email = PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS;
$errors = array();
if ($request->isFormPost()) {
@@ -42,6 +43,14 @@
$pref_vary,
$request->getBool($pref_vary));
}
+
+ if ($request->getStr($prefs_html_email) == 'default') {
+ $preferences->unsetPreference($prefs_html_email);
+ } else {
+ $preferences->setPreference(
+ $prefs_html_email,
+ $request->getBool($prefs_html_email));
+ }
}
$preferences->save();
@@ -58,6 +67,10 @@
? pht('Vary')
: pht('Do Not Vary');
+ $html_emails_default = PhabricatorEnv::getEnvConfig('metamta.html-emails')
+ ? pht('HTML')
+ : pht('plaintext');
+
$re_prefix_value = $preferences->getPreference($pref_re_prefix);
if ($re_prefix_value === null) {
$re_prefix_value = 'default';
@@ -76,11 +89,30 @@
: 'false';
}
+ $html_emails_value = $preferences->getPreference($prefs_html_email);
+ if ($html_emails_value === null) {
+ $html_emails_value = 'default';
+ } else {
+ $html_emails_value = $html_emails_value
+ ? 'true'
+ : 'false';
+ }
+
$form = new AphrontFormView();
$form
->setUser($user);
if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
+ $html_email_control = id(new AphrontFormSelectControl())
+ ->setName($prefs_html_email)
+ ->setOptions(
+ array(
+ 'default' => pht('Use Server Default (%s)', $html_emails_default),
+ 'true' => pht('HTML emails'),
+ 'false' => pht('Plaintext emails'),
+ ))
+ ->setValue($html_emails_value);
+
$re_control = id(new AphrontFormSelectControl())
->setName($pref_re_prefix)
->setOptions(
@@ -101,6 +133,9 @@
))
->setValue($vary_value);
} else {
+ $html_email_control = id(new AphrontFormStaticControl())
+ ->setValue('Server Default ('.$html_emails_default.')');
+
$re_control = id(new AphrontFormStaticControl())
->setValue('Server Default ('.$re_prefix_default.')');
@@ -124,6 +159,7 @@
}
$form
+ ->appendChild($html_email_control)
->appendRemarkupInstructions('')
->appendRemarkupInstructions(
pht(
diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php
--- a/src/applications/settings/storage/PhabricatorUserPreferences.php
+++ b/src/applications/settings/storage/PhabricatorUserPreferences.php
@@ -15,6 +15,7 @@
const PREFERENCE_NO_MAIL = 'no-mail';
const PREFERENCE_MAILTAGS = 'mailtags';
const PREFERENCE_VARY_SUBJECT = 'vary-subject';
+ const PREFERENCE_HTML_EMAILS = 'html-emails';
const PREFERENCE_SEARCHBAR_JUMP = 'searchbar-jump';
const PREFERENCE_SEARCH_SHORTCUT = 'search-shortcut';
diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1865,7 +1865,8 @@
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setMailTags($mail_tags)
->setIsBulk(true)
- ->setBody($body->render());
+ ->setBody($body->render())
+ ->setHTMLBody($body->renderHTML());
foreach ($body->getAttachments() as $attachment) {
$template->addAttachment($attachment);
@@ -1886,14 +1887,18 @@
$template->addHeader('X-Herald-Rules', $herald_header);
}
+ if ($object instanceof PhabricatorProjectInterface) {
+ $this->addMailProjectMetadata($object, $template);
+ }
+
if ($this->getParentMessageID()) {
$template->setParentMessageID($this->getParentMessageID());
}
$mails = $reply_handler->multiplexMail(
- $template,
- array_select_keys($handles, $email_to),
- array_select_keys($handles, $email_cc));
+ $template,
+ array_select_keys($handles, $email_to),
+ array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
@@ -1905,6 +1910,44 @@
return $template;
}
+ private function addMailProjectMetadata(
+ PhabricatorLiskDAO $object,
+ PhabricatorMetaMTAMail $template) {
+
+ $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
+ $object->getPHID(),
+ PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
+
+ if (!$project_phids) {
+ return;
+ }
+
+ // TODO: This viewer isn't quite right. It would be slightly better to use
+ // the mail recipient, but that's not very easy given the way rendering
+ // works today.
+
+ $handles = id(new PhabricatorHandleQuery())
+ ->setViewer($this->requireActor())
+ ->withPHIDs($project_phids)
+ ->execute();
+
+ $project_tags = array();
+ foreach ($handles as $handle) {
+ if (!$handle->isComplete()) {
+ continue;
+ }
+ $project_tags[] = '<'.$handle->getObjectName().'>';
+ }
+
+ if (!$project_tags) {
+ return;
+ }
+
+ $project_tags = implode(', ', $project_tags);
+ $template->addHeader('X-Phabricator-Projects', $project_tags);
+ }
+
+
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return $object->getPHID();
}
@@ -2054,6 +2097,7 @@
array $xactions) {
$headers = array();
+ $html_headers = array();
$comments = array();
foreach ($xactions as $xaction) {
@@ -2064,6 +2108,9 @@
$header = $xaction->getTitleForMail();
if ($header !== null) {
$headers[] = $header;
+ $html_headers[] = phutil_tag('span', array(
+ 'style' => 'color:'.$xaction->getColor()
+ ), $header);
}
$comment = $xaction->getBodyForMail();
@@ -2073,7 +2120,8 @@
}
$body = new PhabricatorMetaMTAMailBody();
- $body->addRawSection(implode("\n", $headers));
+ $body->addRawPlaintextSection(implode("\n", $headers));
+ $body->addRawHTMLSection(implode('<br>', $html_headers));
foreach ($comments as $comment) {
$body->addRawSection($comment);
diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php
--- a/src/infrastructure/daemon/bot/PhabricatorBot.php
+++ b/src/infrastructure/daemon/bot/PhabricatorBot.php
@@ -83,6 +83,8 @@
->connect();
$this->runLoop();
+
+ $this->protocolAdapter->disconnect();
}
public function getConfig($key, $default = null) {
@@ -104,6 +106,7 @@
$handler->runBackgroundTasks();
}
} while (!$this->shouldExit());
+
}
public function writeMessage(PhabricatorBotMessage $message) {
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorBaseProtocolAdapter.php
@@ -22,6 +22,13 @@
abstract public function connect();
/**
+ * Disconnect from the service.
+ */
+ public function disconnect() {
+ return;
+ }
+
+ /**
* This is the spout for messages coming in from the protocol.
* This will be called in the main event loop of the bot daemon
* So if if doesn't implement some sort of blocking timeout
diff --git a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
--- a/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
+++ b/src/infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php
@@ -71,8 +71,13 @@
$ok = @stream_select($read, $write, $except, $timeout_sec = 1);
if ($ok === false) {
- throw new Exception(
- 'socket_select() failed: '.socket_strerror(socket_last_error()));
+ // We may have been interrupted by a signal, like a SIGINT. Try
+ // selecting again. If the second select works, conclude that the failure
+ // was most likely because we were signaled.
+ $ok = @stream_select($read, $write, $except, $timeout_sec = 0);
+ if ($ok === false) {
+ throw new Exception('stream_select() failed!');
+ }
}
if ($read) {
@@ -102,6 +107,8 @@
$len = fwrite($this->socket, $this->writeBuffer);
if ($len === false) {
throw new Exception('fwrite() failed!');
+ } else if ($len === 0) {
+ break;
} else {
$messages[] = id(new PhabricatorBotMessage())
->setCommand('LOG')
@@ -250,8 +257,22 @@
$data);
}
- public function __destruct() {
- $this->write('QUIT Goodbye.');
- fclose($this->socket);
+ public function disconnect() {
+ // NOTE: FreeNode doesn't show quit messages if you've recently joined a
+ // channel, presumably to prevent some kind of abuse. If you're testing
+ // this, you may need to stay connected to the network for a few minutes
+ // before it works. If you disconnect too quickly, the server will replace
+ // your message with a "Client Quit" message.
+
+ $quit = $this->getConfig('quit', pht('Shutting down.'));
+ $this->write("QUIT :{$quit}");
+
+ // Flush the write buffer.
+ while (strlen($this->writeBuffer)) {
+ $this->getNextMessages(0);
+ }
+
+ @fclose($this->socket);
+ $this->socket = null;
}
}
diff --git a/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php b/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
--- a/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
+++ b/src/infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php
@@ -2,7 +2,7 @@
final class PhabricatorDataNotAttachedException extends Exception {
- public function __construct(PhabricatorLiskDAO $dao) {
+ public function __construct($object) {
$stack = debug_backtrace();
// Shift off `PhabricatorDataNotAttachedException::__construct()`.
@@ -19,7 +19,7 @@
}
}
- $class = get_class($dao);
+ $class = get_class($object);
$message =
"Attempting to access attached data on {$class}{$via}, but the data is ".
diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css
--- a/webroot/rsrc/css/application/base/notification-menu.css
+++ b/webroot/rsrc/css/application/base/notification-menu.css
@@ -25,7 +25,7 @@
.device-desktop .phabricator-notification-menu,
.device-tablet .phabricator-notification-menu {
- position: fixed;
+ position: absolute;
width: 360px;
top: 42px;
}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 5, 11:56 AM (6 d, 2 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/mg/6k/xrfjnsqxf47taagj
Default Alt Text
D9375.id24727.diff (47 KB)

Event Timeline